# HTTP Endpoints Source: https://docs.kynesys.xyz/backend/api-reference/http-endpoints Plain HTTP GET endpoints and REST route groups served by the node alongside the JSON-RPC interface. # HTTP Endpoints Besides the JSON-RPC entry point at `POST /`, the node serves a set of plain HTTP endpoints for info, health, discovery, and feature subsystems. All are registered in `src/libs/network/server_rpc.ts` (with feature routes loaded from their own modules). ## Core GET endpoints | Method | Path | Purpose | | ------ | -------------------- | ----------------------------------------------------------------- | | GET | `/` | Greeting payload; echoes the caller's IP (`{ message, yourIP }`). | | GET | `/info` | Node info plus `version` and `version_name`. | | GET | `/health` | Liveness/readiness summary (see below). | | GET | `/health/subsystems` | Slim per-subsystem registry snapshot for dashboards. | | GET | `/version` | Node version string. | | GET | `/publickey` | Node public key (hex). | | GET | `/connectionstring` | Node connection string for peers. | | GET | `/peerlist` | Current known peers. | | GET | `/public_logs` | Public log buffer. | | GET | `/diagnostics` | Diagnostic log buffer. | | GET | `/mcp` | MCP server status (`{ enabled, transport, status }`). | | GET | `/genesis` | Parsed genesis data from the genesis block. | | GET | `/rate-limit/stats` | Rate limiter statistics. | Example: ```bash theme={null} curl http://localhost:53550/version # "x.y.z" ``` ### /health `/health` returns a status summary with `status` (`ok` | `degraded` | `dormant` | `failing`), boot progress, subsystem states, port drift, main-loop heartbeat, and error counters. The HTTP status code reflects health: `200` for ok/degraded/dormant (with an `X-Demos-Dormant: true` header when dormant) and `503` when failing, so load balancers and k8s probes can detect an unhealthy node without parsing the body. For the full response contract see [Health and Readiness](/backend/operations/health-and-readiness). ## Storage program routes Registered by `registerStorageProgramRoutes` (`src/features/storageprogram/routes.ts`). All GET; ACL is enforced via the `identity` request header (anonymous callers are limited to public programs). Addresses use the `stor-{hash}` format. | Method | Path | Purpose | | ------ | ---------------------------------------------------- | -------------------------------------------------- | | GET | `/storage-program/:address` | Read a storage program by address. | | GET | `/storage-program/:address/all` | Read the program's raw data. | | GET | `/storage-program/:address/fields` | List top-level field names. | | GET | `/storage-program/:address/field/:field` | Read a field value. | | GET | `/storage-program/:address/field/:field/item/:index` | Read an array-field item by index. | | GET | `/storage-program/:address/has/:field` | Check whether a field exists. | | GET | `/storage-program/:address/type/:field` | Return a field's value type. | | GET | `/storage-program/owner/:owner` | List programs by owner (`?limit=&offset=`). | | GET | `/storage-program/search?q=name` | Search by program name (`?exact=&limit=&offset=`). | ## ZK routes Registered by `registerZkRoutes` (`src/libs/network/zkMerkle.ts`). | Method | Path | Purpose | | ------ | ------------------------------ | ------------------------------------------------------------- | | GET | `/zk/merkle-root` | Current Merkle root, block number, and leaf count. | | GET | `/zk/merkle/proof/:commitment` | Merkle inclusion proof for a `0x`-prefixed 64-hex commitment. | | GET | `/zk/nullifier/:hash` | Whether a `0x`-prefixed 64-hex nullifier hash has been used. | ```bash theme={null} curl http://localhost:53550/zk/merkle-root # { "rootHash": "0x...", "blockNumber": 1234, "leafCount": 42 } ``` ## TLSNotary routes Registered by `registerTLSNotaryRoutes` (`src/features/tlsnotary/routes.ts`). Only mounted when `TLSNOTARY_ENABLED=true`. | Method | Path | Purpose | | ------ | ------------------- | ------------------------------------------------------------ | | GET | `/tlsnotary/health` | Service health (`healthy` \| `unhealthy` \| `disabled`). | | GET | `/tlsnotary/info` | Service info: enabled flag, port, public key, running state. | | POST | `/tlsnotary/verify` | Verify a base64-encoded `attestation` from the request body. | # JSON-RPC Methods Source: https://docs.kynesys.xyz/backend/api-reference/rpc-methods The node's JSON-RPC over HTTP interface: top-level methods and the nodeCall read-only handler registry. # JSON-RPC Methods Every Demos node exposes a single JSON-RPC entry point at `POST /`. Requests are dispatched by `processPayload` in `src/libs/network/rpcDispatch.ts`; the HTTP server that hosts it lives in `src/libs/network/server_rpc.ts`. ## Request and response shape A request is a JSON object with a `method` string and a `params` array (validated by `isRPCRequest`): ```json theme={null} { "method": "ping", "params": [] } ``` Every method returns an `RPCResponse`: ```json theme={null} { "result": 200, "response": "pong", "require_reply": false, "extra": null } ``` | Field | Type | Meaning | | --------------- | ----------- | -------------------------------------------------------------------------------------------------------------------- | | `result` | number | Status code (HTTP-like: `200` ok, `400` bad request, `401` unauthorized, `500` error, `501` method not implemented). | | `response` | any | Method payload (string, object, array, or number). | | `require_reply` | boolean | Whether the caller is expected to reply (used by peer routines). | | `extra` | any \| null | Optional auxiliary data (e.g. error details, block metadata). | Unknown methods return `result: 501` with `"Method not implemented: "`. The server may attach `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` headers to responses so SDK clients can self-throttle. ## Top-level methods These are the `case` branches of the `processPayload` switch. | Method | Description | | -------------------- | ---------------------------------------------------------------------------------- | | `ping` | Liveness check. Returns `result: 200`, `response: "pong"`. | | `execute` | Submit a transaction bundle (`BundleContent`) for execution via `manageExecution`. | | `nativeBridge` | Process a native bridge operation via `manageNativeBridge`. | | `hello_peer` | Peer handshake/introduction (`manageHelloPeer`). | | `mempool` | Merge a peer's mempool into the local mempool. | | `peerlist` | Exchange/handle peer lists. | | `auth` | Authentication message handling (`manageAuth`). | | `nodeCall` | Dispatch a read-only sub-method from the handler registry (see below). | | `login_request` | Browser login request (`handleLoginRequest`). | | `login_response` | Browser login response (`handleLoginResponse`). | | `consensus_routine` | Consensus protocol routine (`manageConsensusRoutines`). | | `gcr_routine` | Global Chain Registry routine (`manageGCRRoutines`). | | `bridge` | Cross-chain bridge routine (`manageBridges`). | | `web2ProxyRequest` | Proxy a Web2 HTTP request (`handleWeb2ProxyRequest`). | | `rate-limit/unblock` | **SUDO.** Unblock an array of IP addresses on the rate limiter. | | `getCampaignData` | **SUDO.** Return GCR campaign data. | | `awardPoints` | **SUDO.** Award points to accounts (`GCR.awardPoints`). | | `verifyProof` | Verify a ZK identity attestation proof and check nullifier reuse. | ## SUDO-protected methods The following methods are listed in `PROTECTED_ENDPOINTS` and require the caller's authenticated public key to equal the node's `SUDO_PUBKEY`. Unauthorized callers receive `result: 401`, `response: "Unauthorized sender"`: * `rate-limit/unblock` * `getCampaignData` * `awardPoints` See [Protected Endpoints](/backend/operations/protected-endpoints) for how SUDO authentication is configured and enforced. ## nodeCall sub-method registry `nodeCall` takes a single `NodeCall` object in `params[0]` and routes it to a named handler from `handlerRegistry` (`src/libs/network/handlers/index.ts`). Handlers are read-only and grouped by domain. ```json theme={null} { "method": "nodeCall", "params": [{ "method": "getLastBlockNumber" }] } ``` ### Peer handlers Source: `peerHandlers.ts`. | Handler | Description | | ----------------- | ------------------------------ | | `getPeerInfo` | Return this node's peer info. | | `getPeerlist` | Return the known peer list. | | `getPeerlistHash` | SHA-256 hash of the peer list. | | `getPeerIdentity` | This node's public key (hex). | | `getPeerTime` | Current node time (epoch ms). | ### Block handlers Source: `blockHandlers.ts`. | Handler | Description | | -------------------------------- | ------------------------------------------- | | `getGenesisDataHash` | Hash of the genesis data. | | `getPreviousHashFromBlockNumber` | Previous block hash given a block number. | | `getPreviousHashFromBlockHash` | Previous block hash given a block hash. | | `getBlockHeaderByNumber` | Block header by number. | | `getBlockHeaderByHash` | Block header by hash. | | `getLastBlockNumber` | Tip block number. | | `getLastBlock` | Full tip block. | | `getLastBlockHash` | Tip block hash. | | `getBlockByNumber` | Full block by number. | | `getBlocks` | Range/batch of blocks. | | `getBlockByHash` | Full block by hash (`hash` or `blockHash`). | | `getBlockTransactions` | Transactions in a block (`blockHash`). | ### Transaction handlers Source: `transactionHandlers.ts`. | Handler | Description | | ----------------------- | ---------------------------------------------- | | `getTransactions` | Query transactions. | | `getTransactionStatus` | Status of a transaction. | | `getTxByHash` | Single transaction by hash. | | `getTxsByHashes` | Multiple transactions by hashes. | | `getAllTxs` | Deprecated; returns `{}`. | | `getTransactionHistory` | History for an `address` + `type` (paginated). | | `getMempool` | Current mempool contents. | ### Identity handlers Source: `identityHandlers.ts`. | Handler | Description | | --------------------- | -------------------------------------------- | | `getAddressInfo` | GCR account info for an `address`. | | `getAddressNonce` | Current nonce for an `address`. | | `getNativeSubnetsTxs` | Native subnet transactions for a `subnetId`. | | `getTweet` | Fetch tweet data by `tweetUrl`. | | `getDiscordMessage` | Fetch Discord message by `discordUrl`. | | `resolveUdDomain` | Resolve an Unstoppable Domains `domain`. | ### Validator handlers Source: `validatorHandlers.ts`. | Handler | Description | | ------------------ | ---------------------------------------------- | | `getValidatorInfo` | Validator record for an `address`. | | `getValidators` | Validator set (optionally at a `blockNumber`). | | `getStakedAmount` | Staked amount for a validator `address`. | ### Governance handlers Source: `governanceHandlers.ts`. | Handler | Description | | ---------------------- | --------------------------------------------- | | `getNetworkParameters` | Active `NetworkParameters`. | | `getActiveProposals` | Open proposals (pending/approved/activating). | | `getProposalVotes` | Live tally for a `proposalId`. | | `getUpgradeHistory` | Proposals that reached `active`. | ### Storage program handlers Source: `storageProgramHandlers.ts`. ACL-enforced read side. | Handler | Description | | ---------------------------- | ---------------------------------- | | `getStorageProgram` | Read a storage program by address. | | `getStorageProgramAll` | Read the program's raw data. | | `getStorageProgramFields` | List top-level field names. | | `getStorageProgramFieldType` | Type of a field. | | `getStorageProgramItem` | Item from an array field. | | `getStorageProgramValue` | Value of a field. | | `getStorageProgramsByOwner` | Programs owned by an address. | | `hasStorageProgramField` | Whether a field exists. | | `searchStoragePrograms` | Search programs by name. | ### Fork handlers Source: `forkHandlers.ts`. | Handler | Description | | ---------------- | ---------------------------------------------------------------------- | | `getNetworkInfo` | Fork-activation status (e.g. `osDenomination`) plus node version info. | ### TLSNotary handlers Source: `tlsnotaryHandlers.ts`. | Handler | Description | | ------------------------- | -------------------------------------------------- | | `requestTLSNproxy` | Request a TLS attestation proxy for a `targetUrl`. | | `tlsnotary.getInfo` | Notary/proxy URLs and public key. | | `tlsnotary.getToken` | Look up a token by `tokenId` or `txHash`. | | `tlsnotary.getTokenStats` | Aggregate token statistics. | ### L2PS handlers Source: `l2psHandlers.ts`. | Handler | Description | | ---------------------------- | ---------------------------------------------------------------------- | | `getL2PSParticipationById` | Whether this node participates in an `l2psUid`. | | `getL2PSMempoolInfo` | Mempool summary for an `l2psUid`. | | `getL2PSTransactions` | Executed transactions for an `l2psUid` (optionally `since_timestamp`). | | `getL2PSAccountTransactions` | Signed/authenticated account history for an `l2psUid` + `address`. | ### Misc handlers Source: `miscHandlers.ts`. | Handler | Description | | ---------- | ----------------------------------- | | `RELAY_TX` | Receive relayed transactions (DTR). | | `hots` | Easter-egg/diagnostic routine. | # Bridges Source: https://docs.kynesys.xyz/backend/bridges/overview Backend bridge implementations for cross-chain asset transfers and liquidity aggregation. # Bridges Architecture This section covers the backend implementation and architecture of cross-chain bridges in the DEMOS Network. For SDK usage and integration guides, see [SDK Bridges](/sdk/bridges/overview). ## Available Bridges Backend implementation of the Rubic DEX aggregator integration # Rubic Bridge Source: https://docs.kynesys.xyz/backend/bridges/rubic-bridge Node-side handling of the Rubic cross-chain bridge — quotes, executes, and mocks trades on behalf of SDK clients. The Rubic bridge integration lets clients quote and execute cross-chain swaps through a Demos node. The node forwards a `BridgeTradePayload` to the underlying [`RubicBridge`](/sdk/bridges/overview) flow and returns the result over standard RPC. ## Surface The node exposes the bridge over `rpcCall({ method: "bridge", params: [...] })`. The node-side `manageBridges` handler (`src/libs/network/manageBridge.ts`) dispatches two RPC methods: | RPC method | SDK method | Purpose | | --------------- | ------------------------------------------------- | ------------------------- | | `get_trade` | `RubicBridge.getTrade(demos, chain, payload)` | Quote a cross-chain trade | | `execute_trade` | `RubicBridge.executeTrade(demos, chain, payload)` | Submit the trade for real | `chain` is a string from `SupportedChains` (e.g. `"POLYGON"`, `"ETH"`). See the [SDK reference](/sdk/cookbook/swap/crosschain-swap) for end-to-end usage. ## Payload ```ts theme={null} export interface BridgeTradePayload { fromToken: "NATIVE" | "USDC" | "USDT" toToken: "NATIVE" | "USDC" | "USDT" amount: number | string fromChainId: number toChainId: number } ``` `amount` and the chain IDs refer to the **foreign** chain pair (e.g. USDT on Polygon → USDT on Ethereum). `"NATIVE"` is the foreign chain's native token (ETH on EVM, MATIC on Polygon, etc.) — **not** Demos `DEM`. No OS conversion is applied. P4 widens `amount` to `number | string` so callers can pass BigInt-safe wire values for large foreign-chain amounts; the underlying semantics are unchanged. ## Native bridge Demos also ships a native (non-Rubic) bridge for stablecoin transfers across a curated EVM + Solana set. It is exposed via `NativeBridgeMethods` on `@kynesyslabs/demosdk/bridge`. Documenting that surface end-to-end is out of scope for this page — see the SDK barrel export for the available methods (`validateChain`, the supported-chain constants, USDC contract addresses per chain, etc.). # Communications Stack Source: https://docs.kynesys.xyz/backend/communications-stack Overview of the Demos node's networking layers: P2P transport, JSON-RPC, the Web2 proxy, and the real-time messaging servers. # Communications Stack A Demos node speaks several protocols at once. They sit at different layers of the stack, from the binary node-to-node transport up to the WebSocket servers that relay end-to-end encrypted chat. This page is a map of those layers and links to the detailed pages for each subsystem. ## P2P transport (node-to-node) Validators and full nodes talk to each other over the **OmniProtocol**, a binary, length-framed, TLS-secured transport. The protocol is implemented under `src/libs/omniprotocol/`: * **Server side** (`src/libs/omniprotocol/server/`): `OmniProtocolServer`, `TLSServer`, `InboundConnection`, `ServerConnectionManager`. * **Client/transport side** (`src/libs/omniprotocol/transport/`): `PeerConnection`, `TLSConnection`, `ConnectionPool`, `ConnectionFactory`, `MessageFramer`. * **Wire format**: messages carry an `OmniOpcode` (`src/libs/omniprotocol/protocol/opcodes.ts`) such as `PING`, `HELLO_PEER`, `EXECUTE`, `BROADCAST`, `MEMPOOL_SYNC`, `BLOCK_SYNC`, and the consensus opcodes. Authentication, rate limiting, and serialization live in the sibling `auth/`, `ratelimit/`, and `serialization/` folders. Peer lifecycle and message fan-out are managed by: * `src/libs/peer/PeerManager.ts` — the registry of known peers. * `src/libs/network/manageHelloPeer.ts` — the `HELLO_PEER` handshake (`HelloPeerRequest`: a signed `url` + `publicKey` + `syncData`). * `src/libs/network/manageP2P.ts` — `DemosP2P`, a multiton that manages participant-to-participant message sessions (the node acts as a relayer). * `src/libs/communications/broadcastManager.ts` — `BroadcastManager`, which broadcasts new blocks and transactions to the relevant peers. ## JSON-RPC layer Clients and the SDK reach the node through a JSON-RPC interface rather than the binary transport. Each RPC method maps to a routine handler under `src/libs/network/routines/`. The full list of JSON-RPC methods exposed by the node, including `web2ProxyRequest` used by the Web2 proxy described below. ## Higher-level services Relay arbitrary HTTPS requests through a node via the `web2ProxyRequest` RPC method, with deterministic response hashing for attestation. The WebRTC signaling server and the L2PS-backed messaging server that relay end-to-end encrypted instant messages between peers. **Experimental: ActivityPub / Fediverse server.** An experimental ActivityPub-style store-and-serve server exists under `src/features/activitypub/` (`fediverse.ts`, `fedistore.ts`, `feditypes.ts`). It is explicitly marked experimental in the source, the handlers carry `TODO Authentication` markers, and it is not wired into node startup. It is **not production-ready** and is listed here only for completeness. # Real-time Messaging Source: https://docs.kynesys.xyz/backend/communications-stack/messaging The WebRTC signaling server and the L2PS-backed messaging server that relay end-to-end encrypted instant messages between peers. # Real-time Messaging A Demos node ships with two WebSocket-based messaging subsystems. Both relay end-to-end encrypted payloads between peers — the node never sees plaintext — and both persist message metadata on-chain for an audit trail. Clients on the SDK side use the Instant Messaging support to drive these servers. ## WebRTC signaling server Source: `src/features/InstantMessagingProtocol/signalingServer/signalingServer.ts` (peer type in `ImPeers.ts`, message types in `types/IMMessage.ts`, errors in `types/Errors.ts`). This is a Bun WebSocket server (`Bun.serve`) that acts as a central hub for peer discovery, public-key exchange, and message routing between peers. It is instantiated at node startup in `src/index.ts`. ### Port The default port is **3005** (`signalingServerPort` in `src/config/defaults.ts`; the constructor also defaults to `3005`). At startup the node uses `rpcSignalingPort` (env `RPC_SIGNALING_PORT`) if set, otherwise `signalingServerPort`, then picks the next available port from there. ### Message types Each frame is JSON with a `type` and a `payload`. Verified types: | Type | Direction | Purpose | | --------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `register` | client → server | Register a peer with `clientId`, `publicKey`, and a `verification` proof. Server replies with `{ success: true, clientId }`. | | `discover` | client → server | Request the list of connected peer IDs; server replies with `{ peers }`. | | `message` | both | Send `{ targetId, message }` to a peer; the server forwards `{ message, fromId }` to the target. | | `request_public_key` | client → server | Request a peer's public key by `targetId`. | | `public_key_response` | server → client | Reply containing `{ peerId, publicKey }`. | | `peer_disconnected` | server → clients | Broadcast `{ peerId }` when a peer drops. | | `error` | server → client | `{ errorType, details, timestamp }`. | Registration includes a `verification` field (a `SerializedSignedObject`) that the server verifies via the transaction validator pool before accepting the peer. Messages are stored on the blockchain (as `instantMessaging` transactions); if the target is offline the message is persisted to the `OfflineMessage` store and delivered on reconnect (capped at 100 offline messages per sender). ### Error types (`ImErrorType`) `INVALID_MESSAGE`, `CLIENT_ID_TAKEN`, `PEER_NOT_FOUND`, `INVALID_PUBLIC_KEY`, `REGISTRATION_REQUIRED`, `INTERNAL_ERROR`, `INVALID_PROOF`. ## L2PS Messaging Server Source: `src/features/l2ps-messaging/L2PSMessagingServer.ts`. A Bun WebSocket server for messaging scoped to an **L2PS** (Layer-2 Parallel Subnet) network. Peers register against a specific `l2psUid`; discovery, public-key lookup, and routing are all confined to peers within the same L2PS network. Messages are delivered instantly over WebSocket and persisted through the L2PS rollup via an `L2PSMessagingService`. This server is **incomplete / not production-ready**. As of the reviewed source, `L2PSMessagingServer.ts` imports `./L2PSMessagingService` and a local `./types` module that are not present in the directory, and the class is not instantiated anywhere in node startup. Treat the details below as the intended protocol, not a shipped feature. ### Protocol frames Each frame is JSON with `type`, `payload`, and `timestamp`. Inbound frame types handled by the router: | Type | Purpose | | -------------------- | ----------------------------------------------------------------------------------------------------------------------- | | `register` | Register with `{ publicKey, l2psUid, proof }`. Replies `registered` with the list of `onlinePeers` in the same network. | | `send` | Send `{ to, encrypted, messageHash }`. Replies `message_sent` (online) or `message_queued` (offline). | | `history` | Fetch conversation history with a peer: `{ peerKey, before, limit, proof }`. Replies `history_response`. | | `discover` | List peers in the caller's L2PS network. Replies `discover_response`. | | `request_public_key` | Look up a peer's public key (same network only). Replies `public_key_response`. | Server-initiated frames include `registered`, `peer_joined`, `peer_left`, `message`, `message_sent`, `message_queued`, `history_response`, `discover_response`, `public_key_response`, and `error`. ### Registration and proofs * The `publicKey` must be a hex string of at least **64 chars** (`MIN_PUBLIC_KEY_LENGTH`, ed25519 = 64 hex chars) matching `^[0-9a-fA-F]+$`. * The `proof` is a signature over the string `register:{publicKey}:{timestamp}`, verified against the public key via the transaction validator pool. * `history` requests carry their own proof over `history:{peerKey}:{timestamp}`. * The target `l2psUid` must resolve to an existing L2PS network. ### Limits * `MAX_MESSAGE_SIZE` = **256 KB** — max raw WebSocket frame size. * `MAX_CIPHERTEXT_SIZE` = **128 KB** — max encrypted ciphertext size. The `encrypted` payload must contain both `ciphertext` and `nonce`, and a peer cannot send a message to itself. ### Error codes The server emits `error` frames with codes including `INVALID_MESSAGE`, `INTERNAL_ERROR`, `REGISTRATION_REQUIRED`, `INVALID_PROOF`, `L2PS_NOT_FOUND`, and `L2PS_SUBMIT_FAILED`. ## SDK side Client applications interact with these servers through the SDK's Instant Messaging support, which handles registration, key exchange, encryption, and the WebSocket framing described above. See the SDK documentation's Instant Messaging section for the client API. # Web2 Proxy (DAHR) Source: https://docs.kynesys.xyz/backend/communications-stack/web2-proxy-dahr Relay arbitrary HTTPS requests through a Demos node via the web2ProxyRequest RPC method, with deterministic response hashing for attestation. # Web2 Proxy (DAHR) The **DAHR** — *Data Agnostic HTTPS Relay* — lets a client send an arbitrary HTTP(S) request *through* a Demos node instead of making it directly. The node forwards the request to the target, returns the response, and computes deterministic hashes over the response so the result can later be attested. The feature lives under `src/features/web2/`: * `handleWeb2.ts` — entry point; sanitizes the request and asks the factory for a DAHR instance. * `dahr/DAHR.ts` — the relay session itself (owns a session ID and a `Proxy`). * `dahr/DAHRFactory.ts` — singleton that creates and tracks DAHR sessions, expiring them after 24 hours. * `proxy/Proxy.ts`, `proxy/ProxyFactory.ts` — the per-session HTTP proxy server that actually forwards traffic. * `validator.ts` — URL validation and SSRF protection. * `sanitizeWeb2Request.ts` — strips/redacts sensitive headers for logging and storage. ## RPC entry point The proxy is reached through the JSON-RPC method **`web2ProxyRequest`**. * Handler: `src/libs/network/routines/transactions/handleWeb2ProxyRequest.ts` * Parser: `parseWeb2ProxyRequest` in `src/libs/utils/web2RequestUtils.ts` The handler switches on `web2Request.raw.action` (an `EnumWeb2Actions` value): Creates a DAHR session via `handleWeb2`. Returns the serialized DAHR (`{ sessionId, web2Request }`) on success (HTTP-style `result: 200`), or an error with `result: 400`. Looks up the DAHR by `sessionId`, validates/normalizes the target URL, then forwards the request and returns the `IWeb2Result`. Any other action returns `Unsupported action: `. The two action values verified in source are `EnumWeb2Actions.CREATE` and `EnumWeb2Actions.START_PROXY`. ## Session model 1. The client sends a `CREATE` request. `DAHRFactory.instance.createDAHR()` builds a `DAHR`, which generates a unique `sessionId` and constructs a `Proxy` bound to that ID. 2. The factory stores the session in an in-memory map keyed by `sessionId`, tracking `lastAccess`. Sessions older than **24 hours** are cleaned up (their proxy is stopped) on the next `createDAHR` call. 3. The client sends a `START_PROXY` request carrying the `sessionId`. The node spins up a local proxy server (listening on an ephemeral port on `0.0.0.0`), forwards the request to the validated target, and returns the response. 4. Requests to the local proxy are authorized with an `x-dahr-session-id` header that must match the session ID (enforced when `requireAuthForAll` is on, which defaults to production mode). ## Request and result shapes `IWeb2Request` carries a `raw` object describing the request, plus `result`, `hash`, and `signature` fields. The `raw` object includes: * `action` — an `EnumWeb2Actions` value (`CREATE` / `START_PROXY`). * `url` — the target URL (validated and normalized before use). * `method` — the HTTP method (`Web2Method`). * `headers` — optional request headers. `IWeb2Result` is what `Proxy.sendHTTPRequest` resolves to: | Field | Description | | --------------------- | ------------------------------------------------------------------------------------ | | `status` | Numeric HTTP status code from the target. | | `statusText` | HTTP status message. | | `headers` | Response headers from the target. | | `data` | Response body as a string. | | `responseHash` | SHA-256 over the exact UTF-8 bytes of the response body. | | `responseHeadersHash` | SHA-256 over the canonicalized response headers. | | `requestHash` | *(optional)* SHA-256 over the request body bytes, present only when a body was sent. | Header canonicalization for `responseHeadersHash` lowercases keys, drops volatile/hop-by-hop headers (`date`, `set-cookie`, `connection`, `content-length`, etc.), and sorts by key, so the hash is deterministic across intermediaries. ## Security hardening The proxy is built to resist SSRF and credential leakage: * **URL validation** (`validateAndNormalizeHttpUrl`): only `http`/`https` schemes; rejects embedded credentials, missing hostnames, `localhost`, and loopback / private / link-local / reserved IP ranges (IPv4, IPv6, and IPv4-mapped IPv6); canonicalizes host, strips default ports, removes fragments. * **DNS preflight** (`Proxy`): the resolved address is re-checked against the disallowed ranges at request time, in case DNS has changed since validation. * **Header sanitization** (`sanitizeWeb2Request.ts`): sensitive headers such as `authorization`, `cookie`, `x-api-key`, and `x-auth-token` are stripped before storage and redacted before logging. * **TLS verification**: certificate verification follows the node's `PROD` flag (enabled in production, disabled in development). The source notes a `TODO` that Web2 requests may eventually need to be signed and may carry a fee. Treat that as not-yet-implemented. # Addressing Potential Criticisms Source: https://docs.kynesys.xyz/backend/consensus-mechanism/addressing-potential-criticisms Responses to common concerns about PoR-BFT implementation complexity and node manipulation. #### Complexity in Implementation * **Response:** While implementing dynamic shard composition and BFT mechanisms may require sophisticated technology, the benefits far outweigh the complexities. The enhanced security, scalability, and efficiency provided by PoR-BFT are crucial for the long-term success of cryptocurrency networks. #### Potential for Node Manipulation * **Response:** In Phase 1, validators that fall out of sync — whether through latency, divergent state, or active manipulation — are excluded from shard composition for affected blocks via passive sync filtering. The CVSA pseudorandom seed (SHA-256 over recent block hashes) ensures the eligible validator set is shuffled fairly into shards; the sync filter ensures only currently-valid validators enter that pool. **Active slashing of stake for provable misbehaviour is a Phase 2 capability — not yet shipped.** See [Validator Lifecycle](/backend/validator-lifecycle/overview) for current validator state transitions. # Comparative Advantage Source: https://docs.kynesys.xyz/backend/consensus-mechanism/comparative-advantage Comparison of PoR-BFT against PoW, PoS, PoA, and EVM-based consensus mechanisms. ### Comparative Advantage #### Proof of Work (PoW) **Why:** PoW blockchains like Bitcoin are energy-intensive and prone to mining concentration. * **Example:** The high energy consumption of Bitcoin mining raises environmental concerns, contributing significantly to carbon emissions. According to the University of Cambridge's Bitcoin Electricity Consumption Index, Bitcoin’s annual energy consumption is comparable to that of a small country. #### Proof of Stake (PoS) **Why:** While PoS is energy-efficient, it can lead to centralization risks where validators with larger stakes control the network. * **Example:** Ethereum 2.0’s transition to PoS has faced criticism for potential wealth concentration among large stakeholders due to increased incentives for large validators. #### Proof of Authority (PoA) **Why:** PoA relies on fixed, trusted nodes which can limit flexibility and dynamism in network participation. * **Example:** A private blockchain using PoA for enterprise use might suffer from reduced adaptability during peak usage periods. PoR-BFT’s dynamic shard composition adds an additional layer of flexibility by rotating nodes within each shard mathematically to ensure balanced resource utilization through pseudorandom seeds. #### EVM-based Consensus **Why:** EVM-based systems like Ethereum have inherent computational overhead leading to high gas fees. * **Example:** A cryptocurrency dApp on Ethereum might face expensive gas costs for transactions, limiting its widespread adoption. PoR-BFT’s efficiency reduces these costs significantly. #### Comparison Summary | Feature | PoR-BFT | PoW (e.g., Bitcoin) | PoS (e.g., Ethereum 2.0) | PoA | EVM-based Consensus | | ----------------------- | ------------------------------- | ------------------------- | -------------------------- | -------------------- | ------------------------- | | **Consensus Mechanism** | Dynamic Shard Composition + BFT | Mining Power | Stake Participation | Fixed Trusted Nodes | Virtual Machine Execution | | **Scalability** | High | Limited | Moderate | Limited | Limited | | **Security** | Enhanced | Vulnerable to 51% attacks | Less secure than BFT-based | More secure than PoW | Moderate security | | **Energy Efficiency** | High | Low | Moderate | Varies | Moderate | # Conclusion Source: https://docs.kynesys.xyz/backend/consensus-mechanism/conclusion As we continue to innovate in this rapidly evolving field, it is evident that the future of blockchain technology lies with frameworks like PoR-BFT. B... As we continue to innovate in this rapidly evolving field, it is evident that the future of blockchain technology lies with frameworks like PoR-BFT. By embracing these advancements, we can build a more equitable, scalable, and resilient digital economy that benefits all stakeholders. The mathematical rigor underpinning PoR-BFT ensures efficient resource distribution and enhanced security for a more robust and adaptable solution. # Decentralization In PoR-BFT Source: https://docs.kynesys.xyz/backend/consensus-mechanism/decentralization-in-por-bft Node distribution, governance structure, and economic incentives that ensure true decentralization. # Decentralization in PoR-BFT #### Node Distribution Mechanism * **Why:** True decentralization requires balanced distribution of power and resources across the network. * **Example:** Unlike centralized systems where a few nodes control most operations, PoR-BFT shuffles the eligible validator set into per-block shards using the CVSA seed — a deterministic SHA-256 over recent block hashes — so that no operator can predict or influence which validators land in their shard. #### Governance Structure * **Why:** Decentralized governance is crucial for maintaining network autonomy and preventing centralization of power. * **Example:** Traditional blockchain systems often face challenges with governance decisions being influenced by large stakeholders. PoR-BFT implements a weighted voting system based on mathematical proofs, ensuring more democratic decision-making. #### Economic Incentives * **Why:** Proper economic incentives are essential to maintain decentralization and prevent wealth concentration. * **Example:** While some PoS systems favor wealthy participants, PoR-BFT's mathematics-based system rewards and encourages broader participation by considering factors beyond mere stake size, using mathematical formulas to calculate fair shard generation rotation. #### Technical Architecture * **Why:** The technical design must support decentralization at its core. * **Example:** Unlike centralized databases or permissioned blockchains, PoR-BFT's architecture enables: * Distributed node validation through mathematical consensus * Peer-to-peer communication protocols * Decentralized storage solutions * Cross-shard coordination using pseudorandom seeds #### Community Participation * **Why:** Wide community participation strengthens decentralization and network resilience. * **Example:** Unlike systems where technical barriers limit participation, PoR-BFT provides: * Lower hardware requirements through efficient resource utilization * Simplified node operation processes * Community-driven development initiatives * Mathematical verification of participation fairness # Enhanced Security Source: https://docs.kynesys.xyz/backend/consensus-mechanism/enhanced-security Byzantine Fault Tolerance and malicious node exclusion mechanisms in PoR-BFT. #### Byzantine Fault Tolerance (BFT) * **Why:** Traditional consensus mechanisms can be vulnerable to attacks from malicious nodes. * **Example:** PoW blockchains like Bitcoin are susceptible to 51% attacks where a majority of mining power could manipulate the blockchain. BFT ensures that even if some nodes act maliciously, the system can continue operating correctly by mathematically verifying node integrity through pseudorandom seeds. #### Exclusion of Out-of-Sync Nodes (Phase 1) * **Why:** Nodes that fail to keep up with the canonical chain — through bad latency, divergent state, or active malice — are excluded from consensus participation. * **What ships today:** Phase 1 enforces this through **passive sync filtering**: validators that fail health/sync checks are skipped during shard composition for the affected blocks. The CVSA seed itself is a deterministic shard-selection function (SHA-256 over recent block hashes), not a node-evaluation oracle — it ensures a fair shuffle, while sync filtering enforces participation eligibility. **Slashing — active stake removal as a punitive response to provable misbehaviour — is a Phase 2 feature.** Phase 1 does not slash, burn, or otherwise penalise stake. The "exclusion" mechanism today is sync-based eligibility, not stake destruction. # Consensus Mechanism Source: https://docs.kynesys.xyz/backend/consensus-mechanism/overview The landscape of blockchain technology is poised for transformation to achieve widespread adoption and growth. Traditional consensus mechanisms fall s... # Consensus Mechanism The landscape of blockchain technology is poised for transformation to achieve widespread adoption and growth. Traditional consensus mechanisms fall short in terms of scalability, security, and efficiency. The Proof of Reputation (PoR) Byzantine Fault Tolerance (PoR-BFT) framework, through its innovative consensus mechanism, aims to address these challenges by providing a more robust and adaptable solution. # Unparalleled Scalability Source: https://docs.kynesys.xyz/backend/consensus-mechanism/unparalleled-scalability How PoR-BFT achieves high throughput through dynamic shard composition and parallel block validation. #### Dynamic Shard Composition * **Why:** Traditional static node participation in blockchains can lead to congestion and increased transaction costs. * **Example:** Bitcoin’s fixed node structure results in slow transaction processing during peak times, leading to higher fees for users. PoR-BFT's dynamic shard composition ensures efficient resource distribution by rotating nodes within each shard based on a pseudorandom seed generated by the CVSA. #### Concurrent Transaction Application Within a Block * **Why:** Throughput is bottlenecked by how many transactions a block can apply per second, not just by how many blocks the chain can produce. * **What ships today:** Within a single block, the GCR pipeline partitions independent transactions into groups (via union-find over touched entity keys) and applies up to **8 groups concurrently** while preserving sequential semantics within each group. See [How is GCR Synced](/backend/global-change-registry/how-is-gcr-synced) for the implementation. Per-shard consensus itself runs **sequentially** — one consensus round per shard at a time — not multiple rounds in parallel within the same shard. The "parallel" gain in Phase 1 lives in transaction-application within a block (across independent entity groups), not in concurrent consensus rounds. # Installing Dependencies Source: https://docs.kynesys.xyz/backend/developers-testbed/installing-dependencies Installing dependencies is handled entirely through **Bun**. # Installing dependencies Installing dependencies is handled entirely through **Bun**. **Do not** use `npm`, `pnpm`, `yarn`, or any other package manager — they might break some native Bun dependencies. If you're not already inside the repository, move into it: `cd node` Then install the dependencies: `bun install` Sometimes Bun doesn't automatically trust postinstall scripts for new repositories. The warning might look like this: `Blocked 8 postinstalls. Run bun pm untrusted for details.` If you see a warning about trust, run: `bun pm trust --all` This will allow Bun to build the necessary native modules. # Node Configuration Source: https://docs.kynesys.xyz/backend/developers-testbed/node-configuration Reference for the `.env` variables consumed by a Demos node, with the differences between Track 1 (Docker Compose) and Track 2 (bare metal `./run`) called out. # Node configuration A Demos node is configured almost entirely from a single `.env` file. The repo ships `.env.example` as the canonical template: ```bash theme={null} cp .env.example .env # edit .env to override anything you need ``` The defaults are tuned for the **Track 1 (Docker Compose, recommended)** flow. If you are running **Track 2 (bare metal `./run`, advanced)**, two settings differ — see [Track-specific overrides](#track-specific-overrides) below. Old docs referenced `RPC_FEE=5` and `SERVER_PORT`. Both are wrong. The current names are `RPC_PORT` for the listen port and `RPC_FEE` (default `1`, paired with `NETWORK_FEE=1` and `BURN_FEE=1` for a per-tx total of `3`). ## Track-specific overrides | Variable | Track 1 (Docker Compose) | Track 2 (bare metal `./run`) | | ---------------- | ------------------------------------- | ------------------------------------- | | `PG_HOST` | `postgres` (in-network service name) | `localhost` | | `PG_PORT` | `5432` (container-internal) | `5332` (host-mapped from the sidecar) | | `TLSNOTARY_HOST` | `tlsnotary` (in-network service name) | `localhost` | Everything else in `.env` is shared between the two tracks. ## Consensus and governance | Variable | Default | Notes | | --------------------- | --------------------- | --------------------------------------------------------------------------------------------- | | `CONSENSUS_TIME` | `10` | Block production interval in seconds. Lower = faster blocks, higher CPU. | | `RPC_FEE` | `1` | Per-tx fee component. | | `NETWORK_FEE` | `1` | Per-tx fee component. | | `BURN_FEE` | `1` | Per-tx fee component (burned). Total per-tx flat fee = `RPC_FEE + NETWORK_FEE + BURN_FEE`. | | `MIN_VALIDATOR_STAKE` | `1000000000000000000` | Minimum stake to become a validator (raw bigint as string, must fit Postgres int64 ≤ 9.2e18). | These values are the chain's bootstrap defaults at genesis. Governance proposals can override them at runtime — see [Network Governance](/backend/network-governance/overview). ## Network and ports | Variable | Default | Notes | | -------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | `RPC_PORT` | `53550` | Main HTTP RPC listen port. | | `EXPOSED_URL` | `http://localhost:53550` | Public URL advertised to peers and SDK clients. **Loopback default is fine for local dev — set to your reachable address before joining a real network.** | | `OMNI_ENABLED` | `true` | Toggle the OmniProtocol binary RPC server. | | `OMNI_PORT` | `53551` | OmniProtocol TCP server port (auto-derived as `RPC_PORT + 1` if unset). | | `OMNI_MODE` | `OMNI_PREFERRED` | One of `HTTP_ONLY`, `OMNI_PREFERRED`, `OMNI_ONLY`. | | `RPC_SIGNALING_PORT` | `3005` | WebRTC signaling server port (peer discovery handshake for legacy MessagingPeer). | | `RPC_MCP_PORT` | `3001` | MCP (Model Context Protocol) server port for AI agent integrations. | If you set `EXPOSED_URL` to a public IP or DNS name, also open inbound TCP for `RPC_PORT` (53550), `OMNI_PORT` (53551), and `7047` (TLSNotary, if enabled) on your firewall *before* peers start trying to reach you. Do **not** expose `5432` / `5332` (Postgres), `9090` (node metrics), `9091` (Prometheus), or `3000` (Grafana) to the public internet. ## Database (PostgreSQL) | Variable | Default | Notes | | ------------- | --------------- | ---------------------------------------------------------------------------------------------------- | | `PG_HOST` | `postgres` | Track 1: in-network service name. Track 2: set to `localhost`. | | `PG_PORT` | `5432` | Track 1: container-internal. Track 2: set to `5332` (the host-mapped port the `./run` sidecar uses). | | `PG_USER` | `demosuser` | | | `PG_PASSWORD` | `demospassword` | Change before sharing the host. | | `PG_DATABASE` | `demos` | | See [Browsing the Postgres DB via psql](/backend/miscellaneous/browsing-the-postgres-db-via-psql) for connection commands per track. ## Identity and peers | Variable | Default | Notes | | ---------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `IDENTITY_FILE` | `.demos_identity` | Path to the node's ed25519 identity keypair. Auto-generated on first boot. Under Track 1 the entrypoint persists this into `demos_node_state` so it survives container recreation. | | `PEER_LIST_FILE` | `demos_peerlist.json` | Cached peer list. Same persistence treatment as the identity file. | See [Backing up and restoring a node](/backend/miscellaneous/backing-up-and-restoring-a-node) — `.demos_identity` is the single most important file to back up. ## TLSNotary TLSNotary provides cryptographic attestation of HTTPS responses. | Variable | Default | Notes | | ----------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `TLSNOTARY_ENABLED` | `true` | Set to `false` to skip TLSNotary entirely. | | `TLSNOTARY_HOST` | `tlsnotary` | Track 1: in-network service name. Track 2: set to `localhost`. | | `TLSNOTARY_PORT` | `7047` | Attestation server port. | | `TLSNOTARY_MODE` | `docker` | `docker` (sidecar container) or `ffi` (in-process Rust binding). | | `TLSNOTARY_FATAL` | `false` | If `true`, TLSNotary failures crash the node. Keep `false` for graceful degradation. | | `TLSNOTARY_SIGNING_KEY` | *(empty)* | **Leave empty in default Docker mode** — the sidecar manages its own key. Only set when `TLSNOTARY_MODE=ffi` (64-char hex secp256k1 key, or empty to auto-generate into `.tlsnotary-key`). | Under Track 1, keep `TLSNOTARY_ENABLED` aligned with the `tlsnotary` profile in `COMPOSE_PROFILES` — enabling one without the other produces noisy warnings. ## Monitoring | Variable | Default | Notes | | ------------------------ | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `METRICS_ENABLED` | `true` | Exposes Prometheus metrics at `http://:/metrics`. | | `METRICS_PORT` | `9090` | Container-internal port. Pinned to `9090` in compose to match the static scrape target in `monitoring/prometheus/prometheus.yml`. Override only on bare metal. | | `METRICS_HOST` | `0.0.0.0` | Bind address. | | `METRICS_HOST_PORT` | `9090` | Host-mapped port for the metrics endpoint (Track 1 only). Set this if `9090` is already taken on the host. | | `GRAFANA_PORT` | `3000` | Dashboard UI port. | | `GRAFANA_ADMIN_USER` | `admin` | | | `GRAFANA_ADMIN_PASSWORD` | `demos` | **Change this** before opening Grafana to anything outside your machine. | | `PROMETHEUS_PORT` | `9091` | Prometheus server external (host-mapped) port. Internal stays `9090`. | ## Logging and runtime | Variable | Default | Notes | | ----------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `LOG_LEVEL` | `info` | One of `debug`, `info`, `warning`, `error`, `critical`. `debug` produces a wall of per-block consensus JSON — only enable when actually debugging. | | `PROD` | `false` | Set `true` in production (enables DTR production relay and prod-only safety checks). | | `L2PS_ZK_ENABLED` | `false` | Advanced: enable ZK proofs in the L2PS layer. Experimental. | ## Docker Compose only These variables are consumed by `docker-compose.yml` and **not** by the node binary itself, so they only matter on Track 1. | Variable | Default | Notes | | ------------------ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `COMPOSE_PROFILES` | `monitoring,tlsnotary` | Comma-separated list of profiles to bring up. Empty = bare minimum (postgres + node). Other profiles: `full` (adds `node-exporter` for host metrics), `neo4j` (only for CGC/KYC features). | | `IMAGE_NAME` | `demos-node` | Image name. Override to pull a pre-built image (e.g. `ghcr.io/kynesyslabs/node`). | | `IMAGE_TAG` | `local` | Image tag. | ## Optional external API keys `.env.example` ships with these commented out. Uncomment only the ones you need; each unlocks a specific feature: | Variable | Used by | | ----------------------------------- | ---------------------------------------------------------- | | `SOLANA_RPC` | Solana RPC endpoint override (default: public mainnet RPC) | | `ETHERSCAN_API_KEY` | EVM block explorer queries | | `HELIUS_API_KEY` | Solana enhanced RPC (NFTs, DAS API) | | `NOMIS_API_KEY` / `NOMIS_CLIENT_ID` | On-chain reputation scoring | | `RAPID_API_KEY` | Various enrichment routes | | `GITHUB_TOKEN` | GitHub-based identity attestations | | `DISCORD_BOT_TOKEN` | Discord-based identity attestations | | `HUMAN_PASSPORT_API_KEY` | Sybil resistance scoring (Gitcoin Passport) | ## Generating your identity You do not configure the node's keypair manually. On first boot the node: 1. Generates a fresh ed25519 keypair. 2. Writes the private key to `IDENTITY_FILE` (default `.demos_identity`). 3. Writes the public key to a file named `publickey_` in the same directory and prints it to the console. For the exact first-boot procedure on each track, see [Running the node](/backend/developers-testbed/running-the-node). ## Configuring the peerlist `demos_peerlist.json` lists known peers as `{ "": "" }`. Copy the example and edit it: ```bash theme={null} cp demos_peerlist.json.example demos_peerlist.json ``` ```jsonc theme={null} { "0xd0b2be2cb6d...": "https://node2.example.network" } ``` For solo / local dev, the file can stay empty (or contain only your own entry). To join an existing network, seed it with at least one known bootstrap peer — see [Joining the testnet using a custom genesis](/backend/miscellaneous/joining-the-testnet-using-a-custom-genesis) for the full procedure. # Developers Testbed Source: https://docs.kynesys.xyz/backend/developers-testbed/overview This section explains how to launch a Demos node once you have access to the repository. # Developers Testbed This section explains how to launch a Demos node once you have access to the repository. In the following chapters, you'll learn how to set up and start one or more Demos node instances, along with the necessary requirements. Make sure you have access to [https://github.com/kynesyslabs/node](https://github.com/kynesyslabs/node) before proceeding with the instructions. # Running The Node Source: https://docs.kynesys.xyz/backend/developers-testbed/running-the-node Start, stop, and operate a Demos node — Track 1 (Docker Compose, recommended) or Track 2 (bare metal `./run`, advanced). # Running the node There are two supported ways to run a Demos node: * **Track 1 — Docker Compose (recommended).** Single-command bring-up of the full stack: node + Postgres + TLSNotary + monitoring. Pick this unless you have a specific reason not to. * **Track 2 — bare metal `./run` (advanced).** Node binary runs natively on the host via Bun; only Postgres lives in a sidecar container managed by `./run`. Pick this for core development, kernel-level debugging, or TUI-driven operation. Both tracks start from the same repo (`git clone https://github.com/kynesyslabs/node.git`) and the same `.env` file (`cp .env.example .env`). They differ only in *where* the node binary and its dependencies live. For the full install walkthrough see [INSTALL.md](https://github.com/kynesyslabs/node/blob/main/INSTALL.md); for variable-by-variable settings see [Node configuration](/backend/developers-testbed/node-configuration). ## Track 1: Docker Compose (recommended) ### Prerequisites * Docker 20.10+ * The `docker compose` v2 plugin (with a space — the legacy hyphenated `docker-compose` Python script is not supported) * `git` ```bash theme={null} docker --version docker compose version ``` ### Start the node From the repo root: ```bash theme={null} cp .env.example .env # defaults are fine for local dev docker compose up ``` The first run pulls images and builds the node container, which can take a few minutes. Subsequent starts are near-instant. When the stack is healthy: * Node RPC: `http://localhost:53550` (try `curl http://localhost:53550/info`) * Grafana: `http://localhost:3000` (default `admin` / `demos`) * Prometheus: `http://localhost:9091` ### First boot: identity generation On the very first start the node entrypoint: 1. Generates a fresh ed25519 keypair if `.demos_identity` does not exist in the `demos_node_state` volume. 2. Writes the private key into the volume at `.demos_identity`. 3. Writes the public key to a `publickey_` file alongside it and prints it in the container logs. Tail the logs to grab your public key: ```bash theme={null} docker compose logs -f node ``` Your identity now persists across `docker compose down` / `up` cycles. It is destroyed by `docker compose down -v` — back it up first (see [Backing up and restoring a node](/backend/miscellaneous/backing-up-and-restoring-a-node)). ### Common operations ```bash theme={null} # Follow node logs docker compose logs -f node # Follow everything docker compose logs -f # Update to the latest source git pull docker compose up -d --build # Stop containers, KEEP volumes (identity, chain data, dashboards) docker compose down # Stop AND DELETE all volumes — nuclear, destroys identity + state docker compose down -v ``` State persists in named Docker volumes prefixed `demos_`: | Volume | Holds | | --------------------------------------- | ----------------------------------------------------------------------------------- | | `demos_node_state` | `.demos_identity`, `demos_peerlist.json`, `.tlsnotary-key`, `output/` | | `demos_pgdata` | PostgreSQL data directory (chain state, indexes) | | `demos_node_data` | Bundled bootstrap data (`genesis.json`, `evmChains`, `l2ps`) plus runtime artefacts | | `demos_node_logs` | Node logs | | `demos_grafana_data` | Grafana dashboards, users, settings | | `demos_prometheus_data` | Prometheus TSDB | | `demos_neo4j_data` / `demos_neo4j_logs` | Neo4j (only when the `neo4j` profile is on) | The `demos_` prefix is set explicitly in `docker-compose.yml`, so volume names are stable regardless of the directory you ran compose from. ### Compose profiles `COMPOSE_PROFILES` in `.env` controls the optional services. The defaults are tuned for local development. | Profile | Adds | | ------------ | -------------------------------------------------------------------- | | *(none)* | postgres + node — bare minimum | | `tlsnotary` | TLSNotary sidecar (HTTPS attestation) | | `monitoring` | Prometheus + Grafana | | `full` | `node-exporter` (host CPU/RAM/disk metrics) — pair with `monitoring` | | `neo4j` | Neo4j (only for CGC/KYC features) | ```bash theme={null} # Default: postgres + node + tlsnotary + monitoring (uses .env) docker compose up # Minimal: only postgres + node (also set TLSNOTARY_ENABLED=false in .env) COMPOSE_PROFILES= docker compose up # Add host-level metrics COMPOSE_PROFILES=monitoring,tlsnotary,full docker compose up # Add Neo4j (only if you actually need CGC/KYC) COMPOSE_PROFILES=monitoring,tlsnotary,neo4j docker compose up ``` ### Going public Before joining a real Demos network from a machine that other peers should reach: 1. Set `EXPOSED_URL` in `.env` to your reachable address (public IP or DNS), not `localhost`. 2. Open inbound TCP for `RPC_PORT` (53550), `OMNI_PORT` (53551), and `7047` (TLSNotary, if enabled) on your firewall **before** advertising the public URL. 3. Seed `demos_peerlist.json` with bootstrap peers from the team — see [Joining the testnet using a custom genesis](/backend/miscellaneous/joining-the-testnet-using-a-custom-genesis). Do **not** expose `5432` (Postgres), `9090` (node metrics), `9091` (Prometheus), or `3000` (Grafana) to the public internet. Keep them behind your firewall or VPN. ## Track 2: Bare metal `./run` (advanced) In Track 2 the node binary runs natively under Bun, and only Postgres lives in Docker (as a sidecar managed by `./run`). ### Prerequisites * Docker (still needed for the Postgres sidecar) * Bun (via Mise or the direct installer) * Rust (required by `./scripts/install-deps.sh` to build `wstcp`) * A `.env` with the bare-metal overrides: ```env theme={null} PG_HOST=localhost PG_PORT=5332 TLSNOTARY_HOST=localhost ``` The full prerequisite walkthrough lives in [INSTALL.md — Track 2](https://github.com/kynesyslabs/node/blob/main/INSTALL.md#track-2-bare-metal-with-run). ### Start the node ```bash theme={null} ./run ``` That spins up the Postgres sidecar (host-mapped on `5332`), runs the node natively, and opens the TUI. Stopping the node with `Ctrl+C` (or `Q` in the TUI) shuts the sidecar down too. `./run` at the repo root is a thin wrapper that forwards to `scripts/run` (the actual implementation). Always invoke `./run` from the repo root — never call `scripts/run` directly. The wrapper exists so the legacy command path keeps working. ### First boot: identity generation The very first `./run`: 1. Generates a fresh ed25519 keypair if `.demos_identity` does not exist. 2. Writes the private key to `.demos_identity` in the repo root. 3. Writes the public key to `publickey_` and prints it on the console. Press `Ctrl+C` (or `Q` in the TUI) to stop, then edit `.env` / `demos_peerlist.json` before restarting. Set tight permissions on the identity file: ```bash theme={null} chmod 600 .demos_identity ``` ### `./run` flags | Flag | Argument | Description | | ----------------- | -------------- | ---------------------------------------------------------- | | `-p` | `` | Node RPC port (default `53550`). | | `-d` | `` | Postgres host-mapped port (default `5332`). | | `-i` | `` | Identity file path (default `.demos_identity`). | | `-c` | `true`/`false` | Clean the Postgres database on startup (default `false`). | | `-n` | `true`/`false` | Skip `git pull` (useful for custom branches). | | `-u` | `` | Override `EXPOSED_URL`. | | `-l` | `` | Peer list file (default `demos_peerlist.json`). | | `-r` | `` | Force runtime — `bun` only (`node` runtime is deprecated). | | `-b` | `true`/`false` | Restore from the most recent backup in `output/`. | | `-v` | *(no arg)* | Verbose logging. | | `-h` | *(no arg)* | Show the help message. | | `-t` / `--no-tui` | *(no arg)* | Disable the TUI. | Examples: ```bash theme={null} ./run # Start with default settings ./run -p 53551 -d 5333 # Run on custom ports ./run -c true # Clean start (fresh database) ./run -v # Verbose output for troubleshooting ./run -n true # Skip git update ./run -t # Disable the TUI ``` ### TUI controls By default `./run` launches a terminal UI with tabs for **Core**, **Network**, **Chain**, **Consensus**, and others. | Key | Action | | ------------------------------ | --------------------------------------------------------- | | Number keys (`1`, `2`, `3`, …) | Switch to the corresponding tab | | Arrow keys | Scroll within the current tab | | `H` | Help | | `Q` | Quit (graceful shutdown, also stops the Postgres sidecar) | Disable the TUI with `./run -t` or `./run --no-tui` if you prefer plain log output (useful in `tmux`, CI, or when piping to a file). ### Verify the node is running In a separate terminal: ```bash theme={null} # Liveness curl http://localhost:53550 curl http://localhost:53550/info # Confirm ports are bound sudo lsof -i :53550 # node RPC sudo lsof -i :5332 # Postgres sidecar # Confirm the Postgres container is up docker ps ``` ### Stop the node Press `Ctrl+C` (or `Q` in the TUI) in the `./run` terminal. To stop the Postgres sidecar manually: ```bash theme={null} cd postgres_5332 ./stop.sh ``` ### Reset chain state without losing identity ```bash theme={null} ./run -c true # wipe Postgres on startup; identity in .demos_identity is kept ``` If you are connected to peers, the node will resync from them. If you are running solo, the chain restarts from the genesis block. ### Bare-metal troubleshooting | Symptom | Fix | | --------------------------- | ------------------------------------------------------------------------------------- | | Port already in use | `sudo lsof -i :5332` to identify, then `./run -d 5333` (or pick another port). | | Docker permission denied | `sudo usermod -aG docker $USER && newgrp docker`. | | Database connection timeout | `sudo systemctl restart docker`, then `cd postgres_5332 && ./clean.sh && ./start.sh`. | | Missing dependencies | `rm -rf node_modules bun.lockb && bun install`. | # Setting Up The Environment Source: https://docs.kynesys.xyz/backend/developers-testbed/setting-up-the-environment Running a Demos node requires **Bun**. # Setting up the environment Running a Demos node requires **Bun**. Almost any OS and platform is supported, as long as it runs **Bun ≥ 1.2.7**. You can install Bun by following the instructions at [https://bun.sh/docs/installation](https://bun.sh/docs/installation). After installation, make sure Bun is correctly added to your system path by running: `bun -v` If you see the version number, you're good to go. *** #### System Requirements Running a local node doesn’t require much, but here are the minimum and recommended specs: **Minimum Requirements:** * 4 cores / 4 threads modern CPU (x86 or ARM) * 8GB DDR4 RAM * 10GB SSD dedicated space * 100 Mbps internet connection (if connecting with peers) **Suggested Requirements:** * 4 cores / 8 threads modern CPU (x86 or ARM) * 12GB DDR4 RAM * 20GB SSD dedicated space * 200 Mbps internet connection (if connecting with peers) # Setting Up The Repository Source: https://docs.kynesys.xyz/backend/developers-testbed/setting-up-the-repository The first step is to clone and set up the Demos node repository. # Setting up the repository The first step is to clone and set up the Demos node repository. Clone it with: `git clone https://github.com/kynesyslabs/node` Then navigate into the directory: `cd node` Before installing any dependencies, make sure you're on the correct branch: * **Main branch**: stick with `main` to work with stable features — the same powering the nodes listed at [https://status.demos.sh](https://status.demos.sh). * **Testnet branch**: switch to `testnet` if you want to test the latest features (and deal with the bugs that come with them). Only recommended if you've been instructed to, or if you enjoy living dangerously. # Gcr Structure Source: https://docs.kynesys.xyz/backend/global-change-registry/gcr-structure The following schema shows the internal database structure of the GCR. # GCR Structure The following schema shows the internal database structure of the GCR. The GCR is comprised of several native data tables and a metatable. The main ones are described below. GCR Structure ### gcr\_main This is the primary table where the essential properties of a Demos account within the network are stored. Each account is keyed by its `pubkey` and holds its `nonce` and `balance` (see the balance column note below). Cross-context properties — tracked web3/social identities, points, and referral info — are stored in optimized JSONB columns (`identities`, `points`, `referralInfo`), allowing for efficient data management and lower overhead. #### Account structure (`gcr_main` columns) A Demos account is a single `gcr_main` row with the following columns (from `src/model/entities/GCRv2/GCR_Main.ts`): | Column | Type | Description | | ------------------------- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `pubkey` | `text` (primary key) | The account's public key — its network address. | | `nonce` | `integer` | Monotonic transaction counter for replay protection. | | `balance` | `numeric(38,0)` → `bigint` | Account balance in the active denomination (DEM pre-fork, OS post-fork). See the balance column note below. | | `identities` | `jsonb` (`StoredIdentities`) | Cross-context identities — `xm` (web3 wallets), `web2` (social accounts), `pqc` (post-quantum keys), `ud` (Unstoppable Domains). See [Cross-Context Identities](/backend/internal-mechanisms/cross-context-identities). | | `points` | `jsonb` | Reputation points: `totalPoints`, a per-source `breakdown`, and `lastUpdated`. See [Points System](/backend/incentives/points-system). | | `referralInfo` | `jsonb` | `referralCode`, `referredBy`, `referrals[]`, `totalReferrals`. See [Referral System](/backend/incentives/referral-system). | | `flagged` | `boolean` | Whether the account is flagged (e.g. suspected bot / no-activity heuristics). | | `flaggedReason` | `text` | Reason enum: `twitter_bot`, `evm_no_tx`, `solana_no_tx`, `web3_no_tx`, `only_evm_no_tx`, `manualFlag`, `referrerFlagged`, or `""`. | | `reviewed` | `boolean` | Whether the flag has been manually reviewed. | | `createdAt` / `updatedAt` | `timestamp` | Row lifecycle timestamps. | ### gcr\_subnets\_txs This table is responsible for holding and indexing all the transactions that happens in a [Subnet](../l2ps-subnet-framework/). Note that `tx_data` contains the encrypted data for that specific Subnet's tx and can only be decrypted by partecipants of the Subnet itself. See [how-are-l2ps-transactions-handled.md](../l2ps-subnet-framework/how-are-l2ps-transactions-handled) for more informations. ### gcr\_assigned\_txs This table records which transactions have been assigned to which GCR account (`pubkey` → `tx_hash`, with the `block_number` they were applied in). It replaces the `assignedTxs` JSONB array that previously lived on `gcr_main`, eliminating the write-amplification of appending to a growing JSONB column on every block. ### gcr\_hashes This table is separate from the GCR data. As a metadata table, it stores the combined hashes of the GCR's native tables to generate a GCR Status hash. This is crucial for verifying the consistency of GCR statuses during synchronization and consensus processes. ### `gcr_main` — balance column type The newer `gcr_main` table stores per-account balances directly. The `balance` column is **`numeric(38, 0)`** — arbitrary precision, integer-only — widened from the original `bigint` to accommodate the [osDenomination fork](/backend/network-forks/overview): post-fork balances are 10⁹× their pre-fork values (`UPDATE gcr_main SET balance = balance * 1000000000` would otherwise overflow signed 64-bit on production seed magnitudes). At the application boundary the column is read as a TypeScript `bigint` via the `bigintNumericTransformer` (`src/model/entities/transformers.ts`). Existing call sites that worked with the previous `bigint` column continue to work unchanged. Raw `entityManager.query(...)` callers must wrap the value in `BigInt(row.balance)` — this was already the convention because the pg driver also returns `bigint` columns as strings. The `numeric(38, 0)` zero-scale constraint enforces integer-only writes at the column level, comfortably covering the 10²⁷ OS magnitude ceiling. ### Unimplemented GCR edit types GCR mutations are applied by edit type in `src/libs/blockchain/gcr/handleGCR.ts`. A few edit types are recognized by the dispatcher but are **not yet implemented** — they are no-ops on the apply path and should not be relied on: * `escrow` — logs `not yet implemented` and applies nothing. The SDK exposes an `escrow` module, but escrow transactions are **not settled by the node** on current networks. * `smartContract` — Demos-native contract edits are not yet wired (`not yet implemented`). * `assign` and `subnetsTx` — return `"Not implemented"`. Integrators should treat these as forward-looking and not assume on-chain effect until the apply path lands. # How Is GCR Synced Source: https://docs.kynesys.xyz/backend/global-change-registry/how-is-gcr-synced How transactions are partitioned, applied concurrently across independent groups, and merged back into a deterministic GCR state. # How is GCR Synced? GCR synchronization runs every time a block of transactions is applied to state. The pipeline lives in `HandleGCR.applyTransactions` (`/src/libs/blockchain/gcr/handleGCR.ts:636`). It has three phases: **partition**, **concurrent group apply**, and **merge + persist**. ## 1. Partition into independent groups `HandleGCR.partitionIndependentTxs(txs)` (`handleGCR.ts:477`) groups transactions so that no two groups touch the same entity. The implementation uses a **union-find** over namespaced entity keys — sender pubkey, storage program address, TLS notary token id, etc. Two transactions that share any touched entity are unioned into the same group; transactions that touch nothing land in their own singleton group. The invariant: across groups, no two transactions write to the same row. Within a group, the original transaction order is preserved (so e.g. nonce increments on a shared account stay sequential). ## 2. Apply groups concurrently `applyTransactions` runs groups through `runGroup(...)` in parallel waves of **`CONCURRENCY = 8`** at a time (`handleGCR.ts:691`). The fan-out is bounded to protect the Postgres connection pool. Inside each group, transactions apply sequentially — there is no further parallelism within a group, because intra-group transactions are by definition not independent. Each group emits: * `successful` / `failed` tx hash sets * An indexed list of side effects (each tagged with its original tx index for later re-ordering) * A per-sender `assignedTxs` map (sender pubkey → tx hashes), which feeds the validator's tx assignment ledger ## 3. Merge and persist After all waves finish, results are reduced: * `successful` and `failed` sets are unioned across groups. * Side effects are sorted by their original tx index so downstream execution sees the same ordering as a sequential baseline. * `assignedTxs` maps are merged. Senders only appear in one group (the union-find guarantees it), so the merge is conflict-free. A single `gcrWriteMutex.runExclusive(...)` block then persists `saveGCREditChanges(entities, mergedSideEffects)` and, if non-empty, `bulkUpdateAssignedTxs(assignedTxsUpdates)` (`handleGCR.ts:737`). The mutex serializes against any other writer that holds it, so concurrent block applications can't interleave their persistence steps. ## Properties * **Deterministic outcome.** Same input block + same starting state ⇒ same final GCR. The intra-group sequential apply and the index-based side-effect re-sort guarantee replay equivalence regardless of how groups interleave at runtime. * **Bounded parallelism.** Up to 8 groups run concurrently. The mutex around persistence prevents inter-block interleaving. * **No cross-group conflicts by construction.** Union-find guarantees disjoint entity sets per group; the persistence step needs no per-row locking. ## Pre-filter Before partitioning, `applyTransactions` filters out non-state-mutating transactions (`handleGCR.ts:648`). Only the state-touching subset enters the partition. Skipped txs are still tracked for logging/metrics but bypass the apply pipeline. # Global Change Registry Source: https://docs.kynesys.xyz/backend/global-change-registry/overview As the blockchain expands, storing all required information in each block to compute the current state of the blockchain becomes impractical. To addre... # Global Change Registry As the blockchain expands, storing all required information in each block to compute the current state of the blockchain becomes impractical. To address this issue, we have introduced a mechanism known as the Global Change Registry (GCR), as detailed in our yellow paper. The GCR acts as a trusted, shared reference point for the blockchain's state. After synchronization of all blocks, it allows for quick retrieval, setting, and manipulation of account and subnet properties while maintaining both consistency and security. # Points System Source: https://docs.kynesys.xyz/backend/incentives/points-system How Demos nodes award reputation points for linking Web2 and Web3 identities, where points are stored in the GCR, and the public award/deduct method surface. # Points System The points system is a lightweight reputation mechanism: accounts earn points for linking and verifying external Web2 (Twitter, GitHub, Telegram, Discord) and Web3 (wallets, Unstoppable Domains, Nomis, Human Passport, Ethos) identities. It is implemented by the singleton `PointSystem` class (`src/features/incentive/PointSystem.ts`). Points feed into the [Referral System](/backend/incentives/referral-system), which awards extra bonuses on top of the base linking points. ## Point values Base award values (`pointValues` in `PointSystem.ts`): | Action | Points | | ---------------------- | ------ | | `LINK_WEB3_WALLET` | 0.5 | | `LINK_TWITTER` | 2 | | `LINK_GITHUB` | 1 | | `LINK_TELEGRAM` | 1 | | `FOLLOW_DEMOS` | 1 | | `LINK_DISCORD` | 1 | | `LINK_UD_DOMAIN_DEMOS` | 3 | | `LINK_UD_DOMAIN` | 1 | | `LINK_HUMAN_PASSPORT` | 1 | Nomis and Ethos awards are score-tiered rather than fixed: * `getNomisPointsByScore`: 1-5 points based on the normalized score (`>=80 -> 5`, `>=60 -> 4`, `>=40 -> 3`, `>=20 -> 2`, else `1`). * `getEthosPointsByScore`: 1-5 points (`>=2000 -> 5`, `>=1600 -> 4`, `>=1200 -> 3`, `>=800 -> 2`, else `1`). ## Where points are stored Points live in the `points` JSONB column on the account's `GCRMain` row (the GCR): ```typescript theme={null} points: { totalPoints: number breakdown: { web3Wallets: { [chain: string]: number } socialAccounts: { twitter: number github: number discord: number telegram: number } udDomains?: { [domain: string]: number } referrals: number demosFollow: number nomisScores: { [chain: string]: number } humanPassport?: number ethosScores?: { [chain: string]: number } // weeklyChallenge / zkAttestation also tracked } lastUpdated: Date } ``` All award and deduct methods funnel through the private `addPointsToGCR`, which updates the relevant `breakdown` bucket, recomputes `totalPoints`, sets `lastUpdated`, and saves the account. Deductions are floored at zero per bucket. ## Public method surface The methods below are the public surface of `PointSystem`. The corresponding linking/unlinking hooks live in `IncentiveManager` (`src/libs/blockchain/gcr/gcr_routines/IncentiveManager.ts`). ### Read * `getUserPoints(userId)` - returns an `RPCResponse` whose `response` is the user's points plus linked-identity summary and `referralCode`. Reads identity data live from the GCR and backfills a referral code for legacy accounts. ### Award * `awardWeb3WalletPoints(userId, walletAddress, chain, referralCode?)` * `awardTwitterPoints(userId, twitterUserId, referralCode?)` * `awardGithubPoints(userId, githubUserId, referralCode?)` * `awardTelegramPoints(userId, telegramUserId, referralCode?, attestation?)` * `awardTelegramTLSNPoints(userId, telegramUserId, referralCode?)` * `awardDiscordPoints(userId, referralCode?)` * `awardUdDomainPoints(userId, domain, signingAddress, referralCode?)` * `awardNomisScorePoints(userId, chain, nomisScore, referralCode?)` * `awardHumanPassportPoints(userId, referralCode?)` * `awardEthosScorePoints(userId, chain, ethosScore, referralCode?)` ### Deduct * `deductWeb3WalletPoints(userId, walletAddress, chain)` * `deductTwitterPoints(userId)` * `deductGithubPoints(userId, githubUserId)` * `deductTelegramPoints(userId)` * `deductDiscordPoints(userId)` * `deductUdDomainPoints(userId, domain)` * `deductNomisScorePoints(userId, chain)` * `deductHumanPassportPoints(userId)` * `deductEthosScorePoints(userId, chain)` Award methods are idempotent per identity: if a bucket already holds points for that platform/domain/chain, they return `pointsAwarded: 0`. Several awards also enforce prerequisites (for example, wallet, Nomis, and Ethos awards require a linked Twitter account; Telegram awards require verified group membership). ## How awards are triggered Identity-add routines invoked during consensus call the matching `IncentiveManager` hook (`walletLinked`, `twitterLinked`, `githubLinked`, `telegramLinked`, `discordLinked`, `udDomainLinked`, `nomisLinked`, `humanPassportLinked`, `ethosLinked`, and their `...Unlinked` counterparts), which forwards to the `PointSystem` award/deduct method. Any `referralCode` is passed through so the [Referral System](/backend/incentives/referral-system) can apply bonuses. ## SUDO-gated campaign endpoints The RPC dispatcher (`src/libs/network/rpcDispatch.ts`) exposes two points-related campaign endpoints, `getCampaignData` and `awardPoints`. Both are in the `PROTECTED_ENDPOINTS` set and require the sender to be the node's `SUDO_PUBKEY`; other senders receive `401 Unauthorized sender`. See [Protected Endpoints](/backend/operations/protected-endpoints) for details. # Referral System Source: https://docs.kynesys.xyz/backend/incentives/referral-system How Demos nodes generate referral codes, validate them, and award bonus points to referrers and referred users during identity linking. # Referral System The referral system lets a user earn bonus points by inviting new users to the platform. It is implemented by the `Referrals` class (`src/features/incentive/referrals.ts`) and is invoked from the points pipeline during identity-linking operations. Referral state lives in the `referralInfo` field of each account's `GCRMain` row (the GCR). See also the [Points System](/backend/incentives/points-system) page for how points are awarded and stored. ## What it does Every account owns a deterministic `referralCode`. When a new user links their first identity (wallet or social) and includes someone else's referral code, the node: 1. Looks up the referrer account that owns the code. 2. Validates the relationship (not self, not already referred, referee still eligible). 3. Awards a bonus to both parties and records the relationship. ## Bonus constants Defined on the `Referrals` class: ```typescript theme={null} static readonly REFERRER_BONUS = 2 // points to the referrer static readonly REFERRED_USER_BONUS = 1 // points to the new (referred) user ``` Both bonuses are added to `points.totalPoints` and tracked under `points.breakdown.referrals`. ## Referral code generation `Referrals.generateReferralCode(publicKey, options?)` derives a code from an ed25519 public key: 1. Strip an optional `0x` prefix; the key must be 64 hex characters. 2. SHA-256 hash the cleaned key (`Hashing.sha256`). 3. Take the leading bytes of the hash and Base58-encode them (`bs58`). 4. Truncate to the requested length. The default `length` is `12` (\~70 bits of entropy). Options allow other lengths (`8 | 10 | 12 | 16`), an optional 2-character checksum, and a prefix, but the system uses the 12-character default. ```typescript theme={null} const code = Referrals.generateReferralCode("0x" + pubkeyHex) // e.g. "8jKm9Xp2QvR7" ``` ## GCR referral state The `referralInfo` field on `GCRMain` has this shape: ```typescript theme={null} referralInfo: { totalReferrals: number referredBy?: string // pubkey of the referrer, if any referralCode: string referrals: Array<{ referredUserId: string referredAt: string // ISO timestamp pointsAwarded: number }> } ``` ## Key methods ### `findAccountByReferralCode(referralCode): Promise` Locates the account that owns a code using a Postgres JSONB query: ```sql theme={null} WHERE gcr.referralInfo ->> 'referralCode' = :referralCode ``` ### `isEligibleForReferral(account): boolean` Returns `true` only if the account has never been referred before. It returns `false` when any of the following hold: * `referralInfo.referredBy` is set * `referralInfo.referrals` is non-empty * `referralInfo.totalReferrals > 0` * `points.totalPoints > 0` (the account already earned points) ### `isAlreadyReferred(referrerAccount, newUserPubkey): boolean` Checks whether `newUserPubkey` already appears in the referrer's `referralInfo.referrals[]`, preventing the same pair from being counted twice. ### `processReferral(newAccount, referralCode, gcrMainRepository): Promise` Entry point invoked from the points pipeline. It resolves the referrer, then silently returns (no error, no points) if: * the code does not resolve to any account, * the referrer and the new account are the same `pubkey`, or * the new account is already in the referrer's `referrals[]`. Otherwise it calls the private `awardReferralPoints`, which: * adds `REFERRER_BONUS` to the referrer's total and `breakdown.referrals`, increments `totalReferrals`, and appends a `referrals[]` entry; * adds `REFERRED_USER_BONUS` to the new user and sets `referralInfo.referredBy` to the referrer's pubkey; * saves the referrer account (the new account is saved by the caller). ## How a code flows in from the SDK A `referralCode` is supplied by the SDK on identity-linking calls (for example `inferXmIdentity` / `addTwitterIdentity`). It is embedded in the request payload and the GCR edit operation. When an identity transaction is broadcast, `handleIdentityRequest` (`src/libs/network/routines/transactions/handleIdentityRequest.ts`) validates a present `referralCode` up front: it rejects the request if the code resolves to no account ("Referrer account not found") or if the referrer equals the sender ("Referrer and new user are the same"). During consensus, the identity-add routines call into `IncentiveManager` hooks (`walletLinked`, `twitterLinked`, etc.), which forward the `referralCode` to the matching `PointSystem` award method. That method calls `addPointsToGCR`, which invokes `Referrals.processReferral` when a code is present and the account is eligible: ``` editOperation.referralCode (from SDK) -> handleIdentityRequest (broadcast-time validation) -> GCR identity-add routine (consensus) -> IncentiveManager.walletLinked / twitterLinked (..., referralCode) -> PointSystem.awardWeb3WalletPoints / awardTwitterPoints (..., referralCode) -> PointSystem.addPointsToGCR (..., referralCode) -> Referrals.processReferral(account, referralCode, repository) ``` ## How codes are returned A user retrieves their own referral code via `PointSystem.getUserPoints`, whose response includes the `referralCode` field. For legacy accounts created before referral support, `getUserPoints` generates and persists a code on first read. # Cross Context Identities Source: https://docs.kynesys.xyz/backend/internal-mechanisms/cross-context-identities Demos Network employs a variety of techniques for different chains and protocols, which is a core aspect of numerous Demos protocol mechanisms, includ... # Cross Context Identities Demos Network employs a variety of techniques for different chains and protocols, which is a core aspect of numerous Demos protocol mechanisms, including the Gas Tanks mechanism and the overall reputation score for a node or user. The primary mechanism for achieving this is known as Cross Context Identities (CCI). Within the blockchain — specifically in the **GCRMain** entity of the GCRv2 schema — each address carries a JSON object called `identities`. This serves as a registry for the user's various identities across contexts. The column is declared on `GCRMain`: ```typescript theme={null} @Column({ type: "jsonb", name: "identities" }) identities: StoredIdentities ``` `StoredIdentities` keys are context-scoped identifiers; values describe the identity payload per context. The `StoredIdentities` type currently defines **seven identity contexts**: | Context | Source | | --------------- | --------------------------------------------------------- | | `xm` | Cross-chain wallet identity (EVM, Solana, Aptos, etc.) | | `web2` | Web2 social identity (Twitter, GitHub, Discord, Telegram) | | `pqc` | Post-quantum key bindings (Falcon, ML-DSA) | | `ud` | Unstoppable Domains | | `nomis` | Nomis wallet score | | `humanpassport` | Human Passport humanity proof | | `ethos` | Ethos reputation profile | Each context has its own payload shape and validation rules. The full list of GCR routines lives at `src/libs/blockchain/gcr/gcr_routines/`. (TLSNotary attestations are tracked separately in the dedicated `gcr_tlsnotary` table rather than in the `identities` object.) Upon each user transaction on a different chain or context (e.g., a [cross-chain](../../sdk/cross-chain/) transaction or a Web2 transaction involving an API key), the public portion of that identity is saved inside the aforementioned table. It should be noted that the private key or API key is not stored on the blockchain. Instead, it is stored locally, if specified, through the use of wallets or dApps that utilize the Demos SDK. ### Leveraging identities This approach offers a significant advantage in managing multiple identities. To illustrate, consider a scenario where User A wishes to execute a Demos transaction, paying gas with their Solana native tokens. As the user has already interacted with Demos using Solana (e.g., a [cross-chain](../../sdk/cross-chain/) transaction), the blockchain is aware of its Solana public key. The RPC will then request a Solana payment from A to one of our designated gas tanks and will verify that A's public key has successfully completed the transaction. Once this is confirmed, the RPC can proceed with the execution. #### It's all SDK It is important to note that the process described can and should ideally be automatic. Once A has its local Demos wallet with its Solana identity loaded (so without sharing the private key with the blockchain but keeping it locally), the wallet is perfectly able to execute the transaction autonomously after a quick confirmation by the user. This allows for a seamless and easy cross-context system to be applied and executed without hassle. #### Example Schema # Network Time Synchronization Source: https://docs.kynesys.xyz/backend/internal-mechanisms/network-time-synchronization As Demos network is strongly based on correct communication between nodes, the protocol's way to synchronize timestamps and to have a reliable network... # Network Time Synchronization As Demos network is strongly based on correct communication between nodes, the protocol's way to synchronize timestamps and to have a reliable network timestamp is crucial. The `calibrateTime.ts` library provides essential functionality for time synchronization and correction in the Demos network. Here's an overview of its key components and functions: ### NTP Server Configuration The library defines a primary NTP (Network Time Protocol) server and a list of fallback servers to ensure reliable time synchronization. ### Time Calibration: The main function getTimestampCorrection() calculates the time difference between the local system and the NTP server, storing this correction in shared state. ### Network Timestamp Retrieval The getNetworkTimestamp() function provides a corrected network timestamp by applying the stored correction to the current local time. ### Time Delta Calculation The getMeasuredTimeDelta() function performs the actual time synchronization by: * Fetching the NTP time * Calculating the round-trip time * Adjusting for network latency * Computing the difference between the adjusted NTP time and local time ### NTP Time Fetching The library includes functions to fetch time from the primary NTP server (getNtpTime()) and fallback servers (getFallbackNtpTime()) if the primary server fails. ### Example ### Summary This library ensures that all nodes in the Demos network can maintain synchronized time, which is critical for proper communication and consensus mechanisms. By using NTP servers and accounting for network latency, it provides a robust solution for time synchronization across the distributed network. # Internal Mechanisms Source: https://docs.kynesys.xyz/backend/internal-mechanisms/overview Explore the internal workings of the Demos Network including time synchronization and cross-context identities. # Internal Mechanisms # How L2PS Transactions Are Handled Source: https://docs.kynesys.xyz/backend/l2ps-subnet-framework/how-are-l2ps-transactions-handled Lifecycle of an L2PS transaction: client-side encryption, node-side decryption + validation, batch aggregation, ZK proof, and L1 confirmation. # How L2PS Transactions Are Handled This page walks through the lifecycle of an L2PS transaction from client submission to L1 confirmation. ## Client-Side Encryption Before any transaction leaves the user's device, it is encrypted using **AES-GCM** (32-byte key, 12-byte IV, with the GCM authentication tag preserved alongside the ciphertext). The SDK API is the `L2PS` class from `@kynesyslabs/demosdk/l2ps`: ```typescript theme={null} import { Demos } from "@kynesyslabs/demosdk/websdk" import L2PS from "@kynesyslabs/demosdk/l2ps" // Connect to a node and a wallet const demos = new Demos() await demos.connect("https://node2.demos.sh") await demos.connectWallet(mnemonic) // Build the inner transaction (a normal Demos transaction) const innerTx = await demos.transfer( "0xRecipient...", 100_000_000_000n, // 100 DEM in OS ) // Create or rejoin an L2PS instance with the shared subnet keys const subnet = await L2PS.create(privateKeyHex, ivHex) // Wrap the transaction in the L2PS encrypted envelope const encryptedTx = await subnet.encryptTx(innerTx) // encryptedTx.content.type === "l2psEncryptedTx" // encryptedTx.content.data carries an L2PSEncryptedPayload: // { l2ps_uid, encrypted_data (base64 ciphertext), // tag (base64 GCM auth tag), original_hash } ``` For the full SDK reference (factory, instance registry, decrypt, config), see [L2PS SDK](/sdk/websdk/l2ps/overview). The 32-byte AES key and the 12-byte IV must match exactly between every member of the subnet. If they don't match, decryption fails and the transaction is rejected. Distribute these keys out-of-band — the SDK never transmits them. ## Node Processing When an L2PS node receives an `l2psEncryptedTx`: ### Step 1: Decryption & Validation The node looks up the subnet by `l2ps_uid`, decrypts the payload (AES-GCM verifies the auth tag — tampered ciphertext is rejected), then validates the inner transaction against L1 state (signature, nonce, balance) before executing it through the standard L2PS executor. ### Step 2: GCR Edit Generation L2PS uses the **Global Change Registry (GCR)** architecture. Instead of mutating state directly, a transaction emits **GCR edits**: ```typescript theme={null} // GCR edits generated for a transfer (amounts in OS post-fork) [ { type: "balance_edit", account: "0xSender...", delta: -101_000_000_000n }, // amount + 1 DEM L2PS fee { type: "balance_edit", account: "0xRecipient...", delta: +100_000_000_000n }, ] ``` The `1 DEM` L2PS fee is burned (not credited to any account). Total L2PS tx fee = `L2PS_TX_FEE = 1 DEM` per transaction. ### Step 3: Mempool Storage Validated transactions enter the L2PS-specific mempool, keyed by hash and L2PS UID. The mempool stores the encrypted blob, the resolved GCR edits, and the current status (`pending` / `batched` / `confirmed`). ## Batch Aggregation The **L2PS Batch Aggregator** runs per L1 block and bundles pending transactions: Gather pending L2PS transactions from the mempool — capped at `ZK_CIRCUIT_MAX_BATCH_SIZE = 10` per batch. Combine all balance changes into a single set of GCR edits. Create a **PLONK** proof verifying batch validity without revealing content. Single L1 transaction submitted containing the proof and the batch hash. ### Configuration | Setting | Default | Description | | --------------------------- | ------- | ------------------------------ | | `ZK_CIRCUIT_MAX_BATCH_SIZE` | 10 | Maximum transactions per batch | | `L2PS_TX_FEE` | 1 DEM | Per-transaction fee (burned) | ## ZK Proof Generation Each batch carries a **PLONK** zero-knowledge proof. The proof verifies: * All inner-transaction signatures are valid * All GCR edits are arithmetically correct * No double-spending within the batch * Batch integrity (no tampering) Proofs are generated against pre-computed proving keys per batch size (e.g. `keys/batch_5/`, `keys/batch_10/`). ZK proofs let validators verify batch validity **without seeing the actual transaction content**. ## Consensus Integration When a new L1 block is created, **L2PS Consensus** applies the batch: * Pending proofs for the block are loaded. * Each proof is verified. * The batch's GCR edits are applied to L1 state via `HandleGCR.apply(...)`. * Transaction statuses are advanced to `confirmed`. ## Transaction Status Flow Validated locally, GCR edits generated **(instant)** Included in an L1 batch transaction **(per block)** L1 block finalized **(final)** | Status | Meaning | Reversible? | | ----------- | -------------------------------------- | -------------------- | | `pending` | Validated locally, GCR edits generated | Yes (if batch fails) | | `batched` | Included in an L1 batch transaction | Yes (if block fails) | | `confirmed` | L1 block finalized | No | ## Authenticated History Access Unlike L1 transactions (public), L2PS history requires **cryptographic proof of ownership**: ```typescript theme={null} const timestamp = Date.now() const message = `getL2PSHistory:${address}:${timestamp}` const { data: signature } = await demos.signMessage(message) const response = await demos.rpcCall({ method: "getL2PSAccountTransactions", params: [{ l2psUid: "testnet_l2ps_001", address, timestamp, signature, }], }) ``` If the signature is invalid or doesn't match the requested address, the node returns **403 Access Denied**. ## Data Separation L2PS maintains strict data separation for privacy: * **Store:** Full encrypted transactions * **Process:** Decrypt locally * **See:** Transaction content * **Store:** Only batch hashes and proofs * **Process:** Verify ZK proofs * **See:** Zero transaction visibility ## Next Steps Set up L2PS from scratch Use L2PS in your application # L2PS Subnet Framework Source: https://docs.kynesys.xyz/backend/l2ps-subnet-framework/overview Layer 2 Privacy Subnets provide encrypted, private transactions on top of the public L1 blockchain with batch rollups and ZK proofs. # L2PS (Layer 2 Privacy Subnet) Framework L2PS is a privacy-preserving transaction layer that enables **encrypted transactions** on the Demos Network. Unlike public L1 transactions, L2PS transactions are encrypted client-side and only decrypted by authorized network participants. ## Key Features Transactions are encrypted with **AES-GCM** (32-byte key, 12-byte IV, authentication tag) in your browser before leaving your device. Multiple L2PS transactions are bundled into a single L1 transaction per block. **PLONK** zero-knowledge proofs verify batch validity without revealing content. Only address owners can access their private transaction history via cryptographic signatures. ## How It Works ### Transaction Flow Transaction is encrypted with the shared **AES-GCM** key (32 bytes) and IV (12 bytes) in the browser before transmission. Node receives encrypted blob, decrypts, validates signature and balance. Bundles up to 10 transactions per L1 block with ZK proof. Single L1 batch transaction submitted to blockchain. ### Privacy Model | Component | L1 (Public) | L2PS (Private) | | ------------------ | -------------- | ------------------------------- | | **Amount** | Visible to all | Encrypted | | **Recipient** | Visible to all | Encrypted | | **Sender** | Visible to all | Visible (outer tx) | | **History Access** | Anyone | Owner only (signature required) | ## Transaction Lifecycle Every L2PS transaction progresses through three stages: **Instant** - Local node decrypts, validates against L1 state, reserves balance. **Per L1 block** - Transaction included in batch with ZK proof generated. **Final** - L1 block confirmed, transaction is immutable on blockchain. ## Transaction Fee Each L2PS transaction costs **1 DEM**. This creates a deflationary mechanism while ensuring fair access to privacy features. ## Architecture Components | Component | Description | | -------------------- | --------------------------------------------- | | **L2PS Mempool** | Separate storage for encrypted transactions | | **Batch Aggregator** | Bundles transactions per L1 block (max 10 tx) | | **ZK Prover** | Generates PLONK proofs for batch validity | | **L2PS Consensus** | Applies GCR edits to L1 state | | **Hash Service** | Relays consolidated hashes to validators | ## Network Participation To participate in an L2PS network, nodes must: 1. Have the shared **AES-GCM key** (32 bytes / 64 hex chars) and **IV** (12 bytes / 24 hex chars) 2. Be configured with the correct **L2PS UID** 3. Store and sync the L2PS mempool with other participants ```json theme={null} // L2PS Network Configuration { "uid": "testnet_l2ps_001", "enabled": true, "keys": { "private_key_path": "data/l2ps/testnet_l2ps_001/private_key.txt", "iv_path": "data/l2ps/testnet_l2ps_001/iv.txt" } } ``` ## Next Steps Learn how L2PS transactions are processed Set up L2PS from scratch Interactive demo app for testing L2PS # L2PS Quick Start Source: https://docs.kynesys.xyz/backend/l2ps-subnet-framework/quickstart Complete guide to set up and test L2PS (Layer 2 Privacy Subnets) with ZK proofs. # L2PS Quick Start Guide This guide walks you through setting up L2PS from scratch on your Demos node. ## Prerequisites * Running Demos node * Node.js 18+ * Docker (for PostgreSQL) ## 1. Create L2PS Network ### Create Configuration Directory ```bash theme={null} mkdir -p data/l2ps/testnet_l2ps_001 ``` ### Generate Encryption Keys ```bash theme={null} # Generate AES-256 key (32 bytes = 64 hex characters) openssl rand -hex 32 > data/l2ps/testnet_l2ps_001/private_key.txt # Generate IV (12 bytes = 24 hex characters — AES-GCM standard) openssl rand -hex 12 > data/l2ps/testnet_l2ps_001/iv.txt ``` ### Create Config File Create `data/l2ps/testnet_l2ps_001/config.json`: ```json theme={null} { "uid": "testnet_l2ps_001", "enabled": true, "config": { "created_at_block": 0, "known_rpcs": ["http://127.0.0.1:53550"] }, "keys": { "private_key_path": "data/l2ps/testnet_l2ps_001/private_key.txt", "iv_path": "data/l2ps/testnet_l2ps_001/iv.txt" } } ``` ## 2. ZK Proof Setup (Optional) ZK proofs provide cryptographic verification of L2PS batch validity. Without ZK keys, the system still works but batches are submitted without proofs (graceful degradation). ### Install Circom ```bash theme={null} curl -Ls https://scrypt.io/scripts/setup-circom.sh | sh ``` ### Generate ZK Keys ```bash theme={null} npm run l2ps:zk:setup ``` This generates: * `keys/batch_5/` - For 1-5 tx batches (\~37K constraints) * `keys/batch_10/` - For 6-10 tx batches (\~74K constraints) ## 3. Start Node ```bash theme={null} ./run ``` Look for these logs: ``` [L2PS] Loaded network: testnet_l2ps_001 [L2PS Batch Aggregator] Started ``` ## 4. Test with POC App The POC app provides a visual interface to test L2PS transactions. The L2PS POC app is available as a [separate repository](https://github.com/kynesyslabs/l2ps-poc). Clone it to test your L2PS setup. ### Install and Run ```bash theme={null} # Clone the POC app (separate repository) git clone https://github.com/kynesyslabs/l2ps-poc.git cd l2ps-poc npm install npm run dev # Open http://localhost:5173 ``` ### Configure Keys Create `.env` file in the POC app directory: ```bash theme={null} VITE_NODE_URL="http://127.0.0.1:53550" VITE_L2PS_UID="testnet_l2ps_001" VITE_L2PS_AES_KEY="" VITE_L2PS_IV="" ``` **Critical**: Client keys MUST exactly match node keys. Copy the key values from your node's L2PS configuration: ```bash theme={null} # From your node directory, copy key contents cat data/l2ps/testnet_l2ps_001/private_key.txt cat data/l2ps/testnet_l2ps_001/iv.txt ``` Paste these values into your POC app's `.env` file. ## 5. Send Test Transactions ### Via POC App 1. Generate or enter a mnemonic 2. Connect wallet 3. Toggle to **L2PS (Private)** mode 4. Enter recipient and amount 5. Click **Send L2PS Transaction** ### Via CLI ```bash theme={null} # Quick test (5 transactions) npx tsx scripts/send-l2-batch.ts --uid testnet_l2ps_001 # Load test (50 transactions) npx tsx scripts/l2ps-load-test.ts --uid testnet_l2ps_001 --count 50 ``` ## 6. Verify Results Wait for batch aggregation, then check: ### Check Proofs ```bash theme={null} docker exec -it postgres_5332 psql -U demosuser -d demos -c \ "SELECT id, l2ps_uid, transaction_count, status FROM l2ps_proofs ORDER BY id DESC LIMIT 10;" ``` ### Check Mempool Status ```bash theme={null} docker exec -it postgres_5332 psql -U demosuser -d demos -c \ "SELECT status, COUNT(*) FROM l2ps_mempool GROUP BY status;" ``` ### Expected Results For 50 transactions: | Metric | Expected | | --------------------- | ----------------------- | | Proofs in DB | \~5 (1 per batch of 10) | | L1 batch transactions | \~5 | | Mempool status | confirmed | | Total fees burned | 50 DEM | ## Environment Configuration | Variable | Description | Default | | ------------------------------ | -------------------------- | ----------- | | `L2PS_AGGREGATION_INTERVAL_MS` | Batch check interval | 10000 (10s) | | `L2PS_MIN_BATCH_SIZE` | Min transactions to batch | 1 | | `L2PS_MAX_BATCH_SIZE` | Max transactions per batch | 10 | | `L2PS_CLEANUP_AGE_MS` | Cleanup confirmed after | 300000 (5m) | ## Troubleshooting Check `data/l2ps//config.json` exists and JSON is valid. Ensure `private_key.txt` and `iv.txt` exist with valid hex values (64 and 24 chars respectively). POC `.env` keys must exactly match node key files. Use the auto-copy command above. Remember: L2PS requires `amount + 1 DEM` fee. Fund wallet first. Run `npm run l2ps:zk:setup`. System works without ZK (graceful degradation). ## Complete Setup Checklist ```bash theme={null} # 1. Create L2PS network mkdir -p data/l2ps/testnet_l2ps_001 openssl rand -hex 32 > data/l2ps/testnet_l2ps_001/private_key.txt openssl rand -hex 12 > data/l2ps/testnet_l2ps_001/iv.txt # 2. Create config.json (see section 1) # 3. Optional: Setup ZK proofs npm run l2ps:zk:setup # 4. Start node ./run # 5. Setup POC app (separate repository) git clone https://github.com/kynesyslabs/l2ps-poc.git cd l2ps-poc && npm install # 6. Configure POC .env with your node's L2PS keys # Copy key values from: data/l2ps/testnet_l2ps_001/private_key.txt and iv.txt # 7. Run POC npm run dev ``` ## Next Steps Understand the complete L2PS architecture Integrate L2PS into your application # Architecture Source: https://docs.kynesys.xyz/backend/mcp-server/architecture The MCP server is integrated into the main node process following the same patterns as the signaling server: The MCP server is integrated into the main node process following the same patterns as the signaling server: ```typescript title="Node Integration Pattern" theme={null} // From src/index.ts - MCP server startup if (indexState.MCP_ENABLED) { try { const { createDemosMCPServer, createDemosNetworkTools } = await import("./features/mcp") // Get available port indexState.MCP_SERVER_PORT = await getNextAvailablePort(indexState.MCP_SERVER_PORT) // Create server with SSE transport for remote access const mcpServer = createDemosMCPServer({ transport: "sse", port: indexState.MCP_SERVER_PORT, host: "localhost" }) // Add all Demos Network tools const tools = createDemosNetworkTools() tools.forEach(tool => mcpServer.registerTool(tool)) await mcpServer.start() // Track in state indexState.mcpServer = mcpServer getSharedState.isMCPServerStarted = true console.log(`[MAIN] MCP server started on port ${indexState.MCP_SERVER_PORT}`) } catch (error) { console.log("[MAIN] Failed to start MCP server:", error) getSharedState.isMCPServerStarted = false // Continue without MCP (failsafe) } } ``` # Available Tools Source: https://docs.kynesys.xyz/backend/mcp-server/available-tools The MCP server automatically provides these tools when started with your node: The MCP server automatically provides these tools when started with your node: **Network Status Tools** Monitor the health and status of your Demos Network node: ```typescript theme={null} // Available network tools: // - get_network_status: Get comprehensive network status // - get_node_identity: Get node identity and public key info // Example usage (through MCP client): const networkStatus = await mcpClient.callTool("get_network_status", {}) console.log("Server port:", networkStatus.serverPort) console.log("Connection string:", networkStatus.connectionString) console.log("Last block:", networkStatus.lastBlockNumber) const nodeIdentity = await mcpClient.callTool("get_node_identity", {}) console.log("Public key:", nodeIdentity.publicKey) console.log("Public IP:", nodeIdentity.publicIP) ``` **Blockchain Query Tools** Access blockchain data and query block information: ```typescript theme={null} // Available blockchain tools: // - get_last_block: Get the most recent block // - get_block_by_number: Get a specific block by number // - get_chain_height: Get current blockchain height // Example usage (through MCP client): const lastBlock = await mcpClient.callTool("get_last_block", {}) console.log("Latest block number:", lastBlock.number) console.log("Block hash:", lastBlock.hash) const specificBlock = await mcpClient.callTool("get_block_by_number", { blockNumber: 100 }) console.log("Block 100 details:", specificBlock) const chainHeight = await mcpClient.callTool("get_chain_height", {}) console.log("Current chain height:", chainHeight.height) ``` **Peer Management Tools** Monitor and manage network peer connections: ```typescript theme={null} // Available peer tools: // - get_peer_list: Get detailed list of connected peers // - get_peer_count: Get current number of connected peers // Example usage (through MCP client): const peerList = await mcpClient.callTool("get_peer_list", {}) console.log("Connected peers:", peerList.peerCount) peerList.peers.forEach(peer => { console.log(`Peer: ${peer.identity} at ${peer.connectionString}`) }) const peerCount = await mcpClient.callTool("get_peer_count", {}) console.log("Total peers:", peerCount.peerCount) ``` # Client Usage Source: https://docs.kynesys.xyz/backend/mcp-server/client-usage When enabled, the node starts the MCP server with SSE transport, making it accessible remotely: ### Connecting to the MCP Server #### Remote Access (SSE Transport) When enabled (`MCP_ENABLED=true`), the node starts the MCP server with SSE transport, making it accessible remotely: ```typescript theme={null} // Connect to your running Demos Network node's MCP server const serverUrl = "http://localhost:3001" // Use your node's MCP port // For SSE (remote) connections: // SSE endpoint: http://localhost:3001/sse // Message endpoint: POST http://localhost:3001/message ``` #### Local Access (stdio Transport) For local development, you can also create a separate stdio MCP server: ```typescript theme={null} import { createDemosMCPServer, createDemosNetworkTools } from "@/features/mcp" // Create a separate stdio server for development const devServer = createDemosMCPServer({ transport: "stdio" // For local development tools }) const tools = createDemosNetworkTools() tools.forEach(tool => devServer.registerTool(tool)) await devServer.start() ``` # Configuration Source: https://docs.kynesys.xyz/backend/mcp-server/configuration The MCP server is configured through environment variables in your `.env` file: The MCP server is configured through environment variables in your `.env` file: ```bash theme={null} # MCP Server Configuration MCP_ENABLED=true # Enable/disable MCP server (default: false) MCP_SERVER_PORT=3001 # Default MCP server port (default: 3001) RPC_MCP_PORT=3001 # Override port (takes precedence when non-zero) ``` The node reads `RPC_MCP_PORT` first and falls back to `MCP_SERVER_PORT` when `RPC_MCP_PORT` is `0` or unset. `.env.example` ships with `RPC_MCP_PORT=3001`, so both variables resolve to the same port by default. # Monitoring Endpoint Source: https://docs.kynesys.xyz/backend/mcp-server/monitoring-endpoint The node provides a dedicated `/mcp` endpoint to check MCP server status: The node provides a dedicated `/mcp` endpoint to check MCP server status: ```bash theme={null} # Check MCP server status curl http://localhost:53550/mcp # Example response: { "enabled": true, "transport": "sse", "status": "running" } ``` # Overview Source: https://docs.kynesys.xyz/backend/mcp-server/overview MCP (Model Context Protocol) is an open standard that allows AI assistants to connect to external data sources and tools. The Demos Network MCP integr... MCP (Model Context Protocol) is an open standard that allows AI assistants to connect to external data sources and tools. The Demos Network MCP integration provides: * **Dual Transport Support**: stdio for local applications, SSE for remote network access * **Comprehensive Tools**: blockchain queries, network monitoring, and peer management * **Production Ready**: Full error handling, logging, and security considerations * **Type Safe**: Complete TypeScript support with runtime validation The MCP server is OFF by default and must be enabled by setting `MCP_ENABLED=true`. Once enabled, it is started when you run your Demos Network node (`src/index.ts`). The integration follows these steps: 1. **Environment Configuration**: Loads MCP settings from environment variables 2. **Port Management**: Automatically selects available ports using the same system as other services 3. **Failsafe Design**: Node continues running even if MCP server fails to start 4. **State Tracking**: MCP server status is tracked in shared state for monitoring # Troubleshooting Source: https://docs.kynesys.xyz/backend/mcp-server/troubleshooting Enable debug logging for MCP operations: #### Common Issues **MCP Server Not Starting** * Check if port 3001 is available or change `MCP_SERVER_PORT` in your environment * Verify `MCP_ENABLED=true` in your `.env` file * Check node logs for MCP startup errors **Can't Connect to MCP Server** * Verify the node is running and MCP server started successfully * Check the `/mcp` status endpoint: `curl http://localhost:53550/mcp` * Ensure firewall allows connections to the MCP port **Tools Not Working** * Verify the node is fully synced and operational * Check that blockchain data is accessible * Review MCP server logs for tool execution errors #### Debug Mode Enable debug logging for MCP operations: ```bash theme={null} # Add to your .env file LOG_LEVEL=debug ``` # Backing Up And Restoring A Node Source: https://docs.kynesys.xyz/backend/miscellaneous/backing-up-and-restoring-a-node Back up and restore a Demos node identity and chain state for both the Docker Compose stack (Track 1) and the bare-metal `./run` install (Track 2). # Backing up and restoring a node The most important thing to back up is your node's `.demos_identity` file — it holds the ed25519 private key that defines your node on the network. Lose it and you lose that identity permanently. Chain data (Postgres) is recoverable by resyncing from peers; identity is not. The procedure differs by install track: * **Track 1 — Docker Compose (recommended).** Identity, peerlist, TLSNotary key, and runtime artefacts live inside the named volume `demos_node_state`. You back up the volume; chain data lives separately in `demos_pgdata`. * **Track 2 — bare metal `./run` (advanced).** Identity and peerlist are plain files in the repo root; Postgres state lives inside the sidecar container's volume. Treat `.demos_identity` like an SSH private key. Store backups encrypted, and never commit them to a repo. ## Track 1: Docker Compose (recommended) ### What's in each volume | Volume | Holds | | ----------------------- | ----------------------------------------------------------------------------------- | | `demos_node_state` | `.demos_identity`, `demos_peerlist.json`, `.tlsnotary-key`, `output/` | | `demos_pgdata` | PostgreSQL data directory (chain state, indexes) | | `demos_node_data` | Bundled bootstrap data (`genesis.json`, `evmChains`, `l2ps`) plus runtime artefacts | | `demos_node_logs` | Node logs | | `demos_grafana_data` | Grafana dashboards, users, settings | | `demos_prometheus_data` | Prometheus TSDB | The `demos_` prefix is set explicitly in `docker-compose.yml`, so the volume names are stable regardless of the directory you ran compose from. ### Back up the node identity (and node-side state) The minimum you must back up is `demos_node_state`. From the repo root: ```bash theme={null} docker run --rm \ -v demos_node_state:/src \ -v "$PWD":/dst \ alpine tar czf /dst/node-state-backup.tar.gz -C /src . ``` That writes `node-state-backup.tar.gz` into the current directory. The node does not need to be stopped to read this volume, but for a consistent snapshot of `output/` and `.tlsnotary-key`, prefer to run the backup while the node is stopped (`docker compose down`). Store the resulting tarball off-host. It contains your private key. ### Back up chain state (optional) Chain state (`demos_pgdata`) is recoverable by resyncing from peers, so backing it up is optional. If you want a fast restart without resync: ```bash theme={null} docker compose down # quiesce Postgres for a clean snapshot docker run --rm \ -v demos_pgdata:/src \ -v "$PWD":/dst \ alpine tar czf /dst/pgdata-backup.tar.gz -C /src . docker compose up -d ``` For a logical (`pg_dump`) backup instead, see the Track 2 section below — the same `pg_dump` works against the compose Postgres if you exec into the container. ### Restore Restoring reverses the backup: stop the stack, recreate empty volumes, untar into them, and start. ```bash theme={null} # Stop and DELETE the existing volumes (this is destructive — you are # about to overwrite identity + chain state with the backup contents). docker compose down -v # Recreate the volume(s) and restore. Docker auto-creates the volume on # first mount, so no explicit `docker volume create` is needed. docker run --rm \ -v demos_node_state:/dst \ -v "$PWD":/src \ alpine tar xzf /src/node-state-backup.tar.gz -C /dst # Optional: restore chain state too docker run --rm \ -v demos_pgdata:/dst \ -v "$PWD":/src \ alpine tar xzf /src/pgdata-backup.tar.gz -C /dst # Bring the stack back up docker compose up -d ``` The node will reuse the restored `.demos_identity` instead of generating a new one, and (if you restored `demos_pgdata`) skip resyncing from genesis. ## Track 2: Bare metal `./run` (advanced) In Track 2 the identity and peerlist are plain files in the repo root, and Postgres lives in the `./run`-managed sidecar (host port `5332`). ### Back up identity and peerlist ```bash theme={null} # From the node repo root, with the node STOPPED (Ctrl+C / Q) tar czf node-files-backup.tar.gz \ .demos_identity \ publickey_* \ demos_peerlist.json \ .tlsnotary-key \ output/ ``` Set tight permissions on the backup: ```bash theme={null} chmod 600 node-files-backup.tar.gz ``` ### Back up the Postgres sidecar Use `pg_dump` against the host-mapped port: ```bash theme={null} pg_dump -h localhost -p 5332 -U demosuser -d demos -Fc -f demos.dump ``` (The `-Fc` flag writes Postgres's custom binary dump format, which is the fastest to restore.) ### Restore ```bash theme={null} # 1. Stop the node and the sidecar # Press Ctrl+C / Q in the ./run terminal, then: cd postgres_5332 && ./stop.sh && cd .. # 2. Restore identity and peerlist tar xzf node-files-backup.tar.gz # 3. Bring the sidecar back, then restore Postgres cd postgres_5332 && ./clean.sh && ./start.sh && cd .. pg_restore -h localhost -p 5332 -U demosuser -d demos --clean --if-exists demos.dump # 4. Start the node ./run ``` ### Reset chain state without losing identity If you want to wipe blocks and transactions but keep your identity (and any GCR accounts the node knows about), use the `./run` backup/restore flag together with the clean-DB flag: ```bash theme={null} ./run -b true # restore from the most recent dump in output/ ./run -c true # clean Postgres on startup ``` The `-b true` flow dumps GCR accounts to JSON in `output/` before clearing Postgres, then re-imports them when the node restarts; `-c true` triggers the wipe. After the next genesis block is forged, the dumped accounts are re-created at their previous balances. To attach the restored node to an existing network, see [Joining the testnet using a custom genesis](/backend/miscellaneous/joining-the-testnet-using-a-custom-genesis). Re-importing accounts changes your local genesis hash, so any peer you join must agree on that hash. # Browsing The Postgres DB via psql Source: https://docs.kynesys.xyz/backend/miscellaneous/browsing-the-postgres-db-via-psql Connect to the node's Postgres database from either the Docker Compose stack (Track 1) or the bare-metal sidecar (Track 2). # Browsing the Postgres DB via psql The Demos node persists chain state in PostgreSQL. How you reach it depends on which install track you used: * **Track 1 — Docker Compose (recommended).** Postgres runs as a service inside the compose network. You shell into the container with `docker compose exec`, and credentials are baked in. * **Track 2 — bare metal `./run` (advanced).** Postgres runs as a sidecar container with its port mapped to `localhost:5332`. You connect from the host with a local `psql` client. Both tracks ship the same default credentials (from `.env.example`): | Variable | Default | | ------------- | --------------- | | `PG_USER` | `demosuser` | | `PG_PASSWORD` | `demospassword` | | `PG_DATABASE` | `demos` | The defaults are fine for solo / local development. Change `PG_PASSWORD` in `.env` before exposing anything beyond your own machine. Postgres is never meant to be reachable from the public internet on either track. ## Track 1: Docker Compose (recommended) In Track 1, `PG_HOST=postgres` and `PG_PORT=5432` — those are the in-network service name and the container-internal port. You do not need a `psql` client installed on the host; the Postgres image already ships one. From the repo root, with the stack running: ```bash theme={null} docker compose exec postgres psql -U demosuser -d demos ``` That drops you straight into the `demos` database as `demosuser`. No password prompt — `docker compose exec` runs inside the container, where the credentials are already provisioned. If you prefer a one-shot query: ```bash theme={null} docker compose exec postgres psql -U demosuser -d demos -c '\dt' ``` ### Connecting from a host-side client (optional) If you want to use a desktop GUI like DBeaver or `psql` on the host, expose the container port by adding a host mapping in `docker-compose.override.yml`, then point your client at `localhost:` with the same credentials. This is **not** the default and you should only do it on a trusted local machine. ## Track 2: Bare metal `./run` (advanced) In Track 2, only Postgres runs in Docker (as a sidecar managed by `./run`), and its port is mapped to the host as `5332`. Set these in `.env` for the bare-metal path: * `PG_HOST=localhost` * `PG_PORT=5332` You will need a `psql` client installed on the host. On Debian/Ubuntu: ```bash theme={null} sudo apt install -y postgresql-common sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh sudo apt-get install -y postgresql-client-16 ``` Then connect: ```bash theme={null} psql -h localhost -p 5332 -U demosuser -d demos ``` Enter `demospassword` at the prompt. A helper script lives under `postgres_5332/` in the repo for managing the sidecar (start, stop, clean). Use it for lifecycle operations; use `psql` directly for queries. ## First commands inside `psql` Once you are in the `demos=#` prompt, list the tables: ```text theme={null} demos=# \dt ``` You will see the chain's persistent tables. The most useful ones for day-to-day inspection: | Table | What's in it | | ------------------------------------------------ | ---------------------------------------------------------------------- | | `blocks` | Forged blocks, indexed by `number` | | `transactions` | All transactions accepted into a block | | `mempooltx` | Pending transactions waiting to be included | | `validators` | Active validator set with stake/status | | `consensus` | Consensus round state and signatures | | `gcr_main` / `gcr_storageprogram` / `gcr_hashes` | GCR account state, storage-program state, and the rolling state hashes | | `pgp_key_server` | Identity-attestation key material | Some quick exploratory queries: ```sql theme={null} -- Latest 10 blocks SELECT number, hash, status FROM blocks ORDER BY number DESC LIMIT 10; -- Validator set SELECT * FROM validators; -- Recent transactions SELECT hash, type, "from" FROM transactions ORDER BY "blockNumber" DESC LIMIT 20; -- Count GCR accounts SELECT COUNT(*) FROM gcr_main; ``` Exit with `\q`. Schema details (column names, indexes, JSONB shapes) live in the node source under `src/model/entities/`. Treat them as internal to the node — they can change between releases without notice. # Joining The Testnet Using A Custom Genesis Source: https://docs.kynesys.xyz/backend/miscellaneous/joining-the-testnet-using-a-custom-genesis Replace the bundled genesis with a network-specific one so a fresh node can sync against an existing Demos network. Covers Track 1 (Docker Compose) and Track 2 (bare metal). # Joining the testnet using a custom genesis A fresh Demos node ships with a default `data/genesis.json` that boots an isolated single-validator chain. To join an existing network — testnet or a privately restored chain — you need: 1. The **same genesis file** every other peer is using. The genesis hash is checked at startup, and a mismatch is unrecoverable: consensus can never form, and the node will exit. 2. At least one **bootstrap peer** to discover the rest of the network from. Both Track 1 (Docker Compose) and Track 2 (bare metal `./run`) consume the same `data/genesis.json` file — they only differ in *where* the file needs to live for the node process to read it. Do not invent bootstrap hostnames or genesis URLs. Get the canonical `data/genesis.json` and a bootstrap peerlist from the Demos team or from a known healthy peer. Pointing at the wrong genesis simply means the node refuses to sync. ## What's in `genesis.json` `data/genesis.json` defines: * The chain's bootstrap accounts (GCR balances at height 0). * The initial validator set. * The **`forks` section** — activation heights for hard forks (e.g. `osDenomination`). These are loaded into `SharedState.forkConfig` at node startup and gate consensus-relevant code paths. For the fork system in detail see [Network Forks](/backend/network-forks/overview). The short version: every node on a network must agree on the `forks` section, because a different activation height produces a different chain. A genesis snippet with a scheduled fork looks like: ```jsonc theme={null} { "balances": [ "..." ], "validators": [ "..." ], "forks": { "osDenomination": { "activationHeight": 12345 } } } ``` A genesis with no `forks` section, or with `activationHeight: null`, leaves every fork unscheduled — the node behaves bit-identically to a pre-fork node. ### Full genesis schema `data/genesis.json` has the following top-level structure: ```jsonc theme={null} { "properties": { "id": 1, // numeric chain id "name": "DEMOS", // human-readable chain name "currency": "DEM" // native currency ticker }, "mutables": { "minBlocksForValidationOnlineStatus": 4 }, "forks": { "osDenomination": { "activationHeight": 0 }, "gasFeeSeparation": { "activationHeight": null, "treasuryAddress": "0x..." } }, "balances": [ ["0xPUBKEY", "1000000000000000000"] // [pubkey, balance] tuples (OS denomination) ], "timestamp": "1692734616", // genesis block timestamp (unix seconds, string) "status": "confirmed", "validators": [ { "address": "0x...", // validator pubkey "status": "2", // validator state (see Validator Lifecycle) "connection_url": "https://node2.demos.sh", // or null "staked_amount": "1000000000000000000", // raw bigint string "first_seen": 0, "valid_at": 0 } ] } ``` | Field | Description | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `properties` | Chain identity: `id`, `name`, `currency`. | | `mutables` | Tunable genesis values such as `minBlocksForValidationOnlineStatus`. | | `forks` | Per-fork activation heights (see [Network Forks](/backend/network-forks/overview)). Every peer must agree on this section. | | `balances` | Bootstrap account balances as `[pubkey, balance]` tuples. The balance side may be a decimal string, integer, or bigint-string and is coerced to a `bigint` in OS denomination; malformed or negative values cause the node to fail loudly at load. | | `timestamp` | Genesis block timestamp (unix seconds, as a string). | | `status` | Genesis block status (e.g. `confirmed`). | | `validators` | Initial validator set. Each entry: `address`, `status`, `connection_url` (or `null`), `staked_amount` (raw bigint string), `first_seen`, `valid_at`. Validator state codes are documented in [Validator Lifecycle](/backend/validator-lifecycle/overview). | The `balances` array is applied **on top of** any restored snapshot rows (see `mergeGenesisBalances`), so a fresh single-validator chain typically ships with an empty `balances: []`. Account rows themselves follow the [`gcr_main` structure](/backend/global-change-registry/gcr-structure). ## Get the genesis from a healthy peer Every running node exposes its current genesis at `/genesis`. To pull one from a known peer: ```bash theme={null} curl -o data/genesis.json https://NODE_URL/genesis ``` Replace `NODE_URL` with the public URL of a peer the team has told you to bootstrap from. Verify the file is valid JSON before continuing: ```bash theme={null} jq . data/genesis.json > /dev/null && echo "OK" ``` You will also want that peer's public key (from `/info`) so you can put it in your peerlist: ```bash theme={null} curl https://NODE_URL/info ``` ## Track 1: Docker Compose (recommended) Track 1 stores chain runtime data in the named volume `demos_node_data`. The bundled `data/` directory (including the default `genesis.json`) is baked into the node image at build time — to override it, you can either: * **(A)** Edit `data/genesis.json` in the repo and rebuild the image, or * **(B)** Mount your custom genesis over the in-container path via a compose override. ### Option A: edit and rebuild ```bash theme={null} # 1. Replace the genesis file in the repo curl -o data/genesis.json https://NODE_URL/genesis # 2. Stop and DELETE existing volumes — a custom genesis means a new chain, # so the existing demos_pgdata is incompatible and must be wiped. docker compose down -v # 3. Rebuild the node image with the new genesis baked in, and start fresh docker compose up -d --build ``` The `down -v` step is required: starting against the old `demos_pgdata` with a different genesis hash will refuse to sync. Your `.demos_identity` lives in `demos_node_state` and will also be deleted by `down -v` — back it up first if you need to keep it (see [Backing up and restoring a node](/backend/miscellaneous/backing-up-and-restoring-a-node)). ### Option B: bind-mount the genesis Create `docker-compose.override.yml` next to `docker-compose.yml`: ```yaml theme={null} services: node: volumes: - ./data/genesis.json:/app/data/genesis.json:ro ``` Then: ```bash theme={null} docker compose down -v # destroys old chain data; back up identity first docker compose up -d ``` The bind mount is read-only and authoritative — the node reads it at startup just like the baked-in copy. ### Seed the bootstrap peerlist A fresh node has an empty `demos_peerlist.json` and cannot discover peers on its own. Seed the volume before first start: ```bash theme={null} cat > /tmp/demos_peerlist.json <": "" } $EDITOR demos_peerlist.json ``` Example `demos_peerlist.json`: ```json theme={null} { "0xd0b2be2cb6d...": "https://NODE_URL" } ``` Then start the node with `-c true` to wipe the local Postgres (the new genesis is incompatible with the old chain data): ```bash theme={null} ./run -c true ``` ## Genesis hash check On startup the node computes the hash of its loaded `data/genesis.json` and, before syncing the first block from a peer, compares it against the peer's reported genesis hash. If they differ: ``` Genesis hash mismatch: local=0x... remote=0x... ``` The node terminates. Causes are almost always: * The peer is on a different network (e.g. you pointed at mainnet with a testnet genesis). * You edited `data/genesis.json` after first boot without also wiping chain data (`docker compose down -v` on Track 1, `./run -c true` on Track 2). * The peer has rotated genesis (re-deploy after a custom-genesis restore) and you have not pulled the new one. Re-pull the genesis from the peer and start over. Genesis hash is height-0 only. Once both sides agree there, ongoing consensus is governed by the activated forks (see [Network Forks](/backend/network-forks/overview)) and the live validator set. # Miscellaneous Source: https://docs.kynesys.xyz/backend/miscellaneous/overview Additional guides for database management, node backups, and testnet configuration. # Network Forks Source: https://docs.kynesys.xyz/backend/network-forks/overview Hard-fork machinery used to schedule and activate consensus-rule changes on the Demos Network at deterministic block heights. # Network Forks Demos uses a small, explicit hard-fork system to schedule consensus-rule changes at specific block heights. The same mechanism gates wire-format changes, state migrations, and any other rule that cannot vary across replicas without breaking consensus. The first production fork is **`osDenomination`**, which activates the [sub-DEM (OS) denomination](#fork-registry). A second fork, **`gasFeeSeparation`** (DEM-665), splits the single lump-sum gas fee into `network`/`rpc`/`additional` components with per-component burn/treasury/rpc-operator distribution. ## Design goals The fork machinery is intentionally minimal: * **Bit-identical to pre-fork code paths until activation.** A node booting without any forks scheduled is byte-for-byte equivalent to a pre-fork node. There is no implicit behavior change from upgrading the binary alone. * **Deterministic.** Activation is a function of block height, not wall-clock time, so all replicas activate at the same logical point. * **Forward-compatible by construction.** New forks are additive — adding one to the registry does not change the meaning of existing ones. * **Cheap to query.** Activation status is held in shared state and answered without a database round-trip. ## Fork registry Known forks live in `src/forks/forkConfig.ts` as a typed registry: ```typescript theme={null} export type ForkName = "osDenomination" | "gasFeeSeparation" export interface BaseForkConfig { activationHeight: number | null description?: string } // Per-fork variants extend BaseForkConfig with their own payload. export type OsDenominationConfig = BaseForkConfig export interface GasFeeSeparationConfig extends BaseForkConfig { treasuryAddress: string } export type ForkConfig = OsDenominationConfig | GasFeeSeparationConfig export const DEFAULT_FORK_CONFIG: ForkConfigByName = { // Fresh chains boot post-fork by default (activationHeight: 0). // Operators upgrading an existing pre-fork chain must instead pin a // future block height in data/genesis.json. osDenomination: { activationHeight: 0, description: "DEM→OS denomination change. amount field becomes OS string.", }, // gasFeeSeparation stays inactive by default — it also needs a real // treasuryAddress (the placeholder zero address is rejected by the // loader when activationHeight !== null). gasFeeSeparation: { activationHeight: null, description: "Gas fee separation (DEM-665). Splits gas into network/rpc/additional components.", treasuryAddress: PLACEHOLDER_TREASURY_ADDRESS, }, } ``` Adding a fork is a single line in this registry. Removing or renaming one is intentionally hard — the type union is a literal so typos surface at compile time rather than being silently treated as "unknown fork → inactive". | Field | Meaning | | ------------------ | --------------------------------------------------------------------------------------------------------------- | | `activationHeight` | Block height at which the fork rules become active. `null` means **never active** (configured but unscheduled). | | `description` | Human-readable rationale, surfaced for operators and diagnostics. Not consumed by consensus. | ## Activation height & genesis loading Activation heights are loaded from `data/genesis.json` at node startup and hydrated into `SharedState.forkConfig`. A fresh genesis without a `forks` section inherits `DEFAULT_FORK_CONFIG`: `osDenomination` defaults to `activationHeight: 0` (fresh chains boot post-fork), while `gasFeeSeparation` defaults to `null` (configured but unscheduled). Operators upgrading an existing pre-fork chain must pin `osDenomination.activationHeight` to a future block in genesis and roll the upgrade in lock-step with their peers. The loader lives in `src/forks/loadForkConfig.ts`. The runtime registry is a deep copy of `DEFAULT_FORK_CONFIG` (via `cloneDefaultForkConfig()`), so per-instance mutation (e.g. genesis loading) does not leak into the module-level constant. ## Gate function Every consensus-relevant code path that depends on a fork consults the gate function: ```typescript theme={null} import { isForkActive } from "@/forks/forkGates" if (isForkActive("osDenomination", blockHeight)) { // post-fork logic } else { // pre-fork logic } ``` A fork is active iff its `activationHeight` is non-null and the supplied `blockHeight >= activationHeight`. Defensive paths return `false` (inactive) when: * `forkConfig` is missing from `SharedState` (e.g. tests booting without full startup). * The fork name is unknown — compile-time typing should prevent this, but the runtime check protects against explicit casts. ## Where the gate lives The current production gate is wired into: * **Transaction hashing** — the wire serializer routes through `serializerGate` so the digest matches the on-wire shape, pre or post-fork. * **Block hashing** — same gate, applied at the block level so consensus signatures cover the right format. * **Block insertion** — `chainBlocks.insertBlock` checks fork state before applying any post-fork validation rule (e.g. fork-specific state migrations). Any new fork that changes wire format or state must hook into these same gate points. ## State migrations Forks that change persisted state (e.g. `osDenomination` re-denominating balances) ship as **idempotent migrations** under `src/forks/migrations/`. Each migration is recorded in the `fork_state` table so it runs **at most once** per chain. The recorded entry carries forensic data (pre/post sums, lost-value tally, applied-at block) for audit. The osDenomination migration is the canonical example: * Atomic: all three data sources (GCRv2 main balance, legacy GCR JSONB balance, validator stakes) migrate in one transaction or none. * Sum-invariant: `postSumOs == preSumDem * 10^9 - totalValueLostOs` is verified before commit. * Cap policy: legacy JSONB balances that would exceed `Number.MAX_SAFE_INTEGER * 0.9` after multiplication are capped, with the lost OS recorded for accounting. ## RPC: `getNetworkInfo` The node exposes fork status via the `getNetworkInfo` RPC (`src/libs/network/handlers/forkHandlers.ts`). SDK clients use it to choose the correct wire format without an extra round-trip. **Request:** no arguments. **Response:** ```json theme={null} { "forks": { "osDenomination": { "activationHeight": 12345, "activated": false, "currentHeight": 9876 } }, "nodeVersion": { "version": "0.9.8", "gitSha": "…", "branch": "stabilisation", "dirty": false, "buildTime": "…" } } ``` The `forks` object surfaces only `osDenomination` today; `nodeVersion` carries the responding node's build provenance (package version, git SHA, branch, dirty flag, build timestamp) so a single call answers both "is the fork active?" and "which binary is this node running?". | Field | Meaning | | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `activationHeight` | The configured activation height, or `null` if the fork is unscheduled. | | `activated` | Whether the fork's rules are currently active — computed via the canonical `isForkActive` gate so it matches the serializer and validator code paths exactly. | | `currentHeight` | In-memory cache of the latest block height the node has processed (`SharedState.lastBlockNumber`). Lets clients do their own near-fork detection without a separate `getLastBlockNumber` call. | The response is wrapped in `forks.` so that adding a future fork is a strictly additive change rather than a breaking one. The handler is intentionally cheap — it reads from in-memory shared state, not the database. Extra fields on the request payload are ignored, so forward-compatible client extensions don't choke the RPC. ## Fork lifecycle phases The osDenomination rollout introduced a phased pattern that future forks can reuse: | Phase | Scope | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | **P2** | Add the fork to the registry with `activationHeight: null`. Wire gates into the serializer/validator paths. Bit-identical to pre-fork. | | **P3a** | Implement post-fork wire serializer and gate it on `isForkActive`. Still inactive at runtime. | | **P3b** | Implement state migration. Hooked into block insertion but does nothing while the fork is unscheduled. | | **P3c** | Add the `getNetworkInfo` RPC so SDK clients can detect activation. | | **P4 (SDK)** | Ship the post-fork SDK that consults `getNetworkInfo` and emits the new wire format. | | **Activation** | Deploy a genesis update that sets `activationHeight` to a real block. From that height onward, the gate flips to `true` and the new rules apply. | This phased rollout ensures that the network can carry the new code on every node before it ever changes behavior — there is no "big bang" deployment. ## Operator notes * **Inspecting the active configuration** — the loaded registry is held in `SharedState.forkConfig`. Operators can confirm activation by calling `getNetworkInfo` against the local RPC. * **Replaying from genesis** — because activation height is genesis-defined, replaying a chain from genesis with a different `forks` section will produce a different chain. * **Adding a fork to a running network** — requires a coordinated genesis update across validators. The fork is not active for any node until its `activationHeight` block is processed; until then the gate falls through to legacy paths regardless of binary version. ## Related documentation * [Amounts & Denominations (SDK)](/sdk/websdk/transactions/denominations) — how SDK clients consume `getNetworkInfo` and switch wire format. * [Network Governance](/backend/network-governance/overview) — for governable parameters that change without requiring a fork. * [Validator Lifecycle](/backend/validator-lifecycle/overview) — for state transitions that interact with denomination changes. # Stackable Genesis Source: https://docs.kynesys.xyz/backend/network-governance/overview On-chain governance for network parameters: validators propose changes, vote with stake-weighted supermajority, and activation lands deterministically across every node. # Stackable Genesis DEMOS network parameters (`networkFee`, `rpcFee`, `minValidatorStake`, `featureFlags`) are not hard-coded for the lifetime of the chain. Validators agree on changes through on-chain proposals; once activated, the new values **stack** on top of the genesis defaults — every node folds the same active set of upgrades and arrives at the same `NetworkParameters` view without coordination. ## What can be governed | Parameter | Range | Effect when activated | | ------------------- | ------------------------- | -------------------------------------------------- | | `networkFee` | `0`–`5000` | Per-byte fee added to gas for every transaction. | | `rpcFee` | `0`–`5000` | Per-byte fee paid to the entry RPC node. | | `minValidatorStake` | bigint string | Minimum stake required to register as a validator. | | `featureFlags` | `Record` | Toggle protocol features (`l2ps`, `tlsn`, etc.). | Phase 2 will add `blockTimeMs` and `shardSize` to the governable set. For the current phase those are operator-set (env / Config) and non-governable. ## Configuration precedence ``` governance (DB active rows) > env (Config) > hardcoded fallback ``` * **Hardcoded fallback** — last-resort defaults shipped with the binary. * **Env (Config)** — operator-tuned bootstrap values for a fresh chain. Read once at startup, used until governance overrides them. * **Governance** — runtime-active proposals override env values per-key. Refreshed in `loadNetworkParameters()` after every committed block. ### Env variables Operators set initial values via these env vars (loaded by `Config` on startup). Each maps to a field on `NetworkParameters` and can later be overridden by a passed governance proposal. | Variable | Maps to | Default | | --------------------- | -------------------------- | --------------------------------------------------------- | | `NETWORK_FEE` | `networkFee` | `1` | | `RPC_FEE` | `rpcFee` | `1` | | `MIN_VALIDATOR_STAKE` | `minValidatorStake` | `"1000000000000000000"` (10^18) | | `CONSENSUS_TIME` | `blockTimeMs` (× 1000) | `10` (= 10000 ms block time; unset falls back to 1000 ms) | | `SHARD_SIZE` | `shardSize` (Phase 2 only) | `4` | ### Genesis defaults If neither env nor governance defines a value, `getGenesisNetworkParameters()` returns these baked-in fallbacks: | Field | Value | | ------------------- | ---------------------------- | | `blockTimeMs` | `1000` | | `shardSize` | `4` | | `minValidatorStake` | `"1000000000000000000"` | | `networkFee` | `1` | | `rpcFee` | `1` | | `featureFlags` | `{ l2ps: true, tlsn: true }` | ## Proposal lifecycle The block confirming the proposal becomes the **snapshotBlock**. The validator set + their staked amounts are frozen as the basis for vote weighting. Status: `pending`. Validators in the snapshot set cast `voteOnUpgrade` transactions. Each vote's weight = `staked_amount @ snapshotBlock` (frozen — unstaking during the window does not reduce a recorded vote's weight). `approveWeight = Σ weight where approve=true`, `threshold = ceil(2/3 × snapshotWeight)`. Proposal passes iff `approveWeight ≥ threshold` and `snapshotWeight > 0`. Status: `pending → activating` (passed) or `rejected`. Status stays `activating` for at least `GRACE_PERIOD_BLOCKS`. The parameter key is locked: no other proposal targeting the same key will be accepted. `proposedParameters` are folded into `NetworkParameters` deterministically — ordered by `(effectiveAtBlock ASC, proposalId ASC)` so every node arrives at the same result without coordination. Status: `activating → active`. | Constant | Value | Governable? | | ---------------------- | -------------------------- | ---------------------- | | `VOTING_WINDOW_BLOCKS` | 100 | ❌ | | `GRACE_PERIOD_BLOCKS` | 50 | ❌ | | `SUPERMAJORITY` | 2/3 (ceiling-rounded) | ❌ (protocol invariant) | | `MAX_CHANGE_PERCENT` | 50% per proposal | ❌ | | `UNSTAKE_LOCK_BLOCKS` | 1000 (≈ 10× voting window) | ❌ | ## Atomic-with-block guarantees Tally and activation hooks run **inside** the same `dataSource.transaction(...)` that wraps `insertBlock`. The contract: 1. **All-or-nothing.** Governance state mutations commit atomically with the block. A failed routine rolls the block back; nothing partial persists. 2. **No node crash on hook failure.** Routine throws → `insertBlock` throws → block insert rejected → consensus retries with the next proposer. The node logs the error but stays online. 3. **Memory never ahead of DB.** `sharedState.networkParameters` is refreshed by `loadNetworkParameters()` **after** the transaction commits, never inside it. If the transaction rolls back, the refresh is a no-op. 4. **In-memory cache, not per-tx DB lookups.** `resolveDynamicFees()` and `getShard()` read the in-memory snapshot — zero DB queries per transaction, one SELECT per block to refresh the cache. ## Vote integrity * One vote per validator per proposal (DB-enforced via unique constraint). * Vote final — no edits after the block confirming the vote. * Voter must be in the snapshot validator set; new validators registered after `snapshotBlock` are excluded from this vote. * Vote weight is frozen at `snapshotBlock`. Unstaking during the voting window does **not** reduce the recorded weight. ## Multi-node consistency Validator-staking and governance transaction types attach their `gcr_edit` **before** `signValidityData()` in the RPC's `confirmTransaction()`. This puts the synthesized edit into the signed payload so it survives DTR relay and consensus verification untouched. Every node applies the same edits deterministically when the block lands — so all 4 nodes converge on identical `validators` and `network_upgrades` rows. ## Safety bounds Two layers gate every proposal: 1. **Per-proposal change cap (50%).** A proposal that moves any numeric parameter by more than 50% (in either direction) from the current active value is rejected. 2. **Absolute floor/ceiling.** Independent of the cap, every parameter has hard bounds (e.g. `networkFee ∈ [0, 5000]`, `minValidatorStake ≥ 1% of genesis stake`). Bounds are themselves non-governable. Both checks run client-side in the SDK and server-side at RPC entry — out-of-range proposals are rejected before consuming a snapshot block. ## Why staking ships first Phase 0 (validator staking) is a hard prerequisite for Phase 1 (governance): without on-chain stake, there's no weight to base a 2/3 supermajority on. Staking introduces the lock-period economics that makes vote-and-run attacks unattractive (10× voting window of locked collateral after `unstake`). ## Related Stake, top-up, unstake, exit transactions Propose, vote, query proposals # Health and Readiness Source: https://docs.kynesys.xyz/backend/operations/health-and-readiness The /health, /health/subsystems, and /info endpoints a Demos node exposes for load balancers, Kubernetes probes, and uptime monitoring. # Health and Readiness A Demos node exposes plain HTTP `GET` endpoints on its RPC port for operational monitoring. These are unauthenticated, return JSON, and are designed to be polled by load balancers, Kubernetes liveness/readiness probes, and uptime checks. The contract is additive: existing fields are preserved across releases so probes do not break. ## `GET /health` The primary health endpoint. It aggregates the subsystem registry, the boot tracker, the main-loop heartbeat, and process-level error counters into a single snapshot. ### Status semantics The top-level `status` field collapses everything into one of four values, with the following precedence (`failing` wins, then `dormant`, `degraded`, `ok`): | `status` | Meaning | HTTP code | | ---------- | ----------------------------------------------------------------------------------------------------------------- | --------- | | `ok` | All subsystems healthy, main loop alive. | `200` | | `degraded` | At least one non-critical subsystem is `failed`, but the chain and main loop are alive. | `200` | | `dormant` | The node booted with an empty peer list and intentionally skipped signaling/MCP/TLSNotary/main loop. | `200` | | `failing` | The mempool/DB is unreachable, the `chain` subsystem failed, or the main loop died (and the node is not dormant). | `503` | Only `failing` returns HTTP `503`. `dormant` and `degraded` are intentional states and return `200`, so a probe keyed solely on HTTP status will not flap on a node that legitimately has no peers yet. ### Headers * `X-Demos-Dormant: true` — added only when `status` is `dormant`. Lets an LB route around a node that is up but not yet participating in consensus. ### Response fields | Field | Type | Notes | | -------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `version` / `version_name` | string | Node version and codename. | | `accepting` | boolean | True when the node is synced and not currently in its sync loop. | | `mempool_size` | number \| null | Pending transaction count. `null` when `Mempool.count()` fails (forces `status: failing`). | | `uptime_s` | number | Seconds since process start. | | `status` | string | `ok` \| `degraded` \| `dormant` \| `failing`. | | `dormant` | boolean | Raw dormant-mode flag. | | `boot` | object | Boot-step roll-up: `complete`, `steps_total`, `steps_ready`, `steps_failed`, `steps_skipped`, `current` (name of the running step, or `null`). | | `subsystems` | object | Per-subsystem snapshot (see below). | | `ports` | object | Per-subsystem `{ requested, actual, drifted }`. `drifted` is true when the bound port differs from the requested one. Only subsystems with a known port appear. | | `main_loop` | object | `heartbeat_age_s` (seconds since last tick, or `null`), `iterations_total`, `exited`, `exit_reason`. | | `errors` | object | `uncaught_total`, `unhandled_rejection_total`, `last_uncaught`. | Each entry under `subsystems` is keyed by name (one of `chain`, `rpc`, `metrics`, `signaling`, `mcp`, `tlsnotary`, `omni`, `dtr`, `l2ps`, `main_loop`) and carries: `status` (`pending` | `running` | `ready` | `failed` | `skipped` | `dormant`), `since` (ms timestamp of the last transition or `null`), `port`, `requestedPort`, `lastError`, `enabled`, and optional `extra`. ### Sample response ```json theme={null} { "version": "1.4.2", "version_name": "mycelium", "accepting": true, "mempool_size": 12, "uptime_s": 8421, "status": "ok", "dormant": false, "boot": { "complete": true, "steps_total": 9, "steps_ready": 8, "steps_failed": 0, "steps_skipped": 1, "current": null }, "subsystems": { "chain": { "status": "ready", "since": 1716800000000, "port": null, "requestedPort": null, "lastError": null, "enabled": true }, "rpc": { "status": "ready", "since": 1716800000100, "port": 53550, "requestedPort": 53550, "lastError": null, "enabled": true }, "metrics": { "status": "ready", "since": 1716800000200, "port": 9090, "requestedPort": 9090, "lastError": null, "enabled": true }, "mcp": { "status": "skipped", "since": 1716800000300, "port": null, "requestedPort": null, "lastError": null, "enabled": false, "extra": { "reason": "MCP disabled" } }, "main_loop": { "status": "ready", "since": 1716800001000, "port": null, "requestedPort": null, "lastError": null, "enabled": true } }, "ports": { "rpc": { "requested": 53550, "actual": 53550, "drifted": false }, "metrics": { "requested": 9090, "actual": 9090, "drifted": false } }, "main_loop": { "heartbeat_age_s": 1, "iterations_total": 5821, "exited": false, "exit_reason": null }, "errors": { "uncaught_total": 0, "unhandled_rejection_total": 0, "last_uncaught": null } } ``` ### Probe configuration A liveness probe should treat any HTTP `503` (i.e. `status: failing`) as unhealthy. A readiness probe wanting to gate traffic until consensus participation can additionally inspect `dormant` / the `X-Demos-Dormant` header or require `boot.complete === true`. ## `GET /health/subsystems` A slim sibling for ops dashboards that only need subsystem state. It returns the same `subsystems` snapshot plus the `dormant` flag, with a smaller body and no status roll-up: ```json theme={null} { "dormant": false, "subsystems": { "chain": { "status": "ready", "since": 1716800000000 } } } ``` ## `GET /info` Returns identity and peering metadata rather than health. Useful for service-discovery and confirming which node answered: ```json theme={null} { "version": "1.4.2", "version_name": "mycelium", "identity": "0x", "connectionString": "http://node.example:53550", "peerlist": [] } ``` # Metrics Reference Source: https://docs.kynesys.xyz/backend/operations/metrics-reference The Prometheus metrics a Demos node exposes on /metrics, including names, types, labels, and histogram buckets. # Metrics Reference A Demos node ships a built-in Prometheus exporter. When enabled, a dedicated HTTP server serves the standard `GET /metrics` endpoint (and a simple `/health` / `/healthz` returning `{"status":"ok"}`) on its own port, separate from the RPC server. Every metric name is prefixed with `demos_`. In addition to the metrics below, the exporter also emits the default `prom-client` process/Node.js metrics (memory, CPU, event loop, GC), which are also `demos_`-prefixed. ## Enabling Metrics are controlled by the `metrics` config block: | Config key | Default | Description | | ----------------- | --------- | ----------------------------------- | | `metrics.enabled` | `true` | Master switch for the exporter. | | `metrics.port` | `9090` | Port the metrics HTTP server binds. | | `metrics.host` | `0.0.0.0` | Bind address. | See [Node Configuration](/backend/developers-testbed/node-configuration) for how these map to environment variables and the Docker Compose / Grafana stack that scrapes this endpoint. Collected gauges are refreshed on a 2.5s interval; counters are incremented at their call sites. ## Core metrics ### System | Metric | Type | Labels | | --------------------------- | ----- | -------------------- | | `demos_node_uptime_seconds` | gauge | — | | `demos_node_info` | gauge | `version`, `node_id` | ### Consensus and chain | Metric | Type | Labels / Buckets | | ---------------------------------------- | --------- | ------------------------------------ | | `demos_consensus_rounds_total` | counter | — | | `demos_consensus_round_duration_seconds` | histogram | buckets: `0.1, 0.5, 1, 2, 5, 10, 30` | | `demos_block_height` | gauge | — | | `demos_mempool_size` | gauge | — | ### Network and peers | Metric | Type | Labels / Buckets | | ------------------------------- | --------- | -------------------------------------------------------------------------------- | | `demos_peers_connected` | gauge | — | | `demos_peers_total` | gauge | — | | `demos_messages_sent_total` | counter | `type` | | `demos_messages_received_total` | counter | `type` | | `demos_peer_latency_seconds` | histogram | buckets: `0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5` (aggregated, no per-peer label) | ### Transactions | Metric | Type | Labels / Buckets | | -------------------------------------- | --------- | ----------------------------------------------------------- | | `demos_transactions_total` | counter | `type`, `status` | | `demos_transactions_failed_total` | counter | `type`, `reason` | | `demos_tps` | gauge | — | | `demos_transaction_processing_seconds` | histogram | `type`; buckets: `0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5` | ### API | Metric | Type | Labels / Buckets | | ------------------------------------ | --------- | ---------------------------------------------------------------------------------------- | | `demos_api_requests_total` | counter | `method`, `endpoint`, `status_code` | | `demos_api_request_duration_seconds` | histogram | `method`, `endpoint`; buckets: `0.001, 0.005, 0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10` | | `demos_api_errors_total` | counter | `method`, `endpoint`, `error_code` | ### IPFS and GCR | Metric | Type | Labels | | ----------------------------- | ------- | ----------- | | `demos_ipfs_pins_total` | gauge | — | | `demos_ipfs_storage_bytes` | gauge | — | | `demos_ipfs_peers` | gauge | — | | `demos_ipfs_operations_total` | counter | `operation` | | `demos_gcr_accounts_total` | gauge | — | | `demos_gcr_total_supply` | gauge | — | ## Observability metrics These mirror the subsystem registry and boot tracker that also back the [`/health`](/backend/operations/health-and-readiness) endpoint. | Metric | Type | Labels | Notes | | ----------------------------------- | ------- | ----------- | ------------------------------------------------------------------- | | `demos_subsystem_up` | gauge | `subsystem` | `1` when status is `ready`/`running`, else `0`. | | `demos_subsystem_skipped` | gauge | `subsystem` | `1` when intentionally skipped. | | `demos_subsystem_uptime_seconds` | gauge | `subsystem` | Seconds since the last state transition. | | `demos_subsystem_restart_total` | counter | `subsystem` | Transitions back to `running` after `failed`/`ready`. | | `demos_subsystem_error_total` | counter | `subsystem` | Errors recorded via `subsystemError()`. | | `demos_boot_step_status` | gauge | `step` | Enum: `0`=pending, `1`=running, `2`=ready, `3`=failed, `4`=skipped. | | `demos_boot_complete` | gauge | — | `1` when all boot steps are ready/skipped. | | `demos_dormant_mode` | gauge | — | `1` when the node booted with no peers. | | `demos_main_loop_heartbeat_seconds` | gauge | — | Seconds since the last main-loop tick. | | `demos_main_loop_iterations_total` | counter | — | Main-loop iterations since boot. | | `demos_port_drift` | gauge | `subsystem` | Actual minus requested port (`0` normally). | | `demos_uncaught_exception_total` | counter | `source` | `uncaughtException` handler invocations. | | `demos_unhandled_rejection_total` | counter | `source` | `unhandledRejection` handler invocations. | ## Collector metrics Sampled live from node state and the host on each collection tick. ### Block detail | Metric | Type | Labels | | -------------------------------- | ----- | ------ | | `demos_last_block_tx_count` | gauge | — | | `demos_seconds_since_last_block` | gauge | — | | `demos_last_block_timestamp` | gauge | — | ### Host system | Metric | Type | Labels | | ----------------------------------- | ----- | ------ | | `demos_system_cpu_usage_percent` | gauge | — | | `demos_system_memory_used_bytes` | gauge | — | | `demos_system_memory_total_bytes` | gauge | — | | `demos_system_memory_usage_percent` | gauge | — | | `demos_system_load_average_1m` | gauge | — | | `demos_system_load_average_5m` | gauge | — | | `demos_system_load_average_15m` | gauge | — | ### Network I/O (per interface) | Metric | Type | Labels | | ------------------------------------- | ----- | ----------- | | `demos_system_network_rx_bytes_total` | gauge | `interface` | | `demos_system_network_tx_bytes_total` | gauge | `interface` | | `demos_system_network_rx_rate_bytes` | gauge | `interface` | | `demos_system_network_tx_rate_bytes` | gauge | `interface` | ### Service and peer health | Metric | Type | Labels | | ----------------------------------- | ----- | -------------------------------------------------- | | `demos_service_docker_container_up` | gauge | `container` | | `demos_service_port_open` | gauge | `port`, `service` | | `demos_peer_online_count` | gauge | — | | `demos_peer_offline_count` | gauge | — | | `demos_peer_info` | gauge | `peer_id`, `url`, `status` (capped at 20 peers) | | `demos_node_http_health` | gauge | `endpoint` (`1`=responding) | | `demos_node_http_response_time_ms` | gauge | `endpoint` | | `demos_node_metadata` | gauge | `version`, `version_name`, `identity` (always `1`) | ## Sample scrape ```text theme={null} # HELP demos_block_height Current block height # TYPE demos_block_height gauge demos_block_height 184213 # HELP demos_subsystem_up Whether the named subsystem is in 'ready' or 'running' state (1) or not (0) # TYPE demos_subsystem_up gauge demos_subsystem_up{subsystem="chain"} 1 demos_subsystem_up{subsystem="rpc"} 1 demos_subsystem_up{subsystem="mcp"} 0 # HELP demos_boot_complete 1 when every registered boot step is in ready/skipped, 0 otherwise # TYPE demos_boot_complete gauge demos_boot_complete 1 ``` # Protected Endpoints Source: https://docs.kynesys.xyz/backend/operations/protected-endpoints The SUDO-gated RPC methods a Demos node exposes (rate-limit/unblock, getCampaignData, awardPoints), how authorization works, and their parameters. # Protected Endpoints A small set of RPC methods are reserved for the node operator. They are gated on the caller's authenticated public key matching the node's configured SUDO key. Any other sender is rejected before the handler runs. ## Authorization Protected methods are dispatched through the same `POST /` RPC entry point as every other method, but are checked against an allow-list before execution: ```text theme={null} PROTECTED_ENDPOINTS = { "rate-limit/unblock", "getCampaignData", "awardPoints" } ``` When the requested `method` is in this set, the dispatcher compares the authenticated `sender` (the request's recovered public key) against `getSharedState.SUDO_PUBKEY`. If they do not match, the call returns immediately: ```json theme={null} { "result": 401, "response": "Unauthorized sender", "require_reply": false, "extra": null } ``` The `401` is returned inside the RPC response body (the HTTP status is still `200`). Clients must inspect `result`, not the HTTP code, to detect an authorization failure on these methods. ### Configuring the SUDO key `SUDO_PUBKEY` is sourced from the `core.sudoPubkey` config key, which defaults to `null`. While it is unset, no sender can satisfy the check, so every protected method is effectively closed. Set it to the operator's public key (hex) to enable these methods. | Config key | Default | Description | | ----------------- | ------- | ---------------------------------------------------- | | `core.sudoPubkey` | `null` | Public key authorized to call protected RPC methods. | See [RPC Methods](/backend/api-reference/rpc-methods) for the full method list and how requests are signed and authenticated. ## Methods ### `rate-limit/unblock` Lifts rate-limit blocks on one or more client IPs. * **Params**: a JSON array of IP address strings (the `params` array itself). A non-array input returns `result: 400` with `"Invalid input. Expected an array of strings."` * **Returns**: `result: 200` with `response.message` and a per-IP `response.results` summary. ```json theme={null} { "method": "rate-limit/unblock", "params": ["203.0.113.5", "198.51.100.20"] } ``` ### `getCampaignData` Returns the current incentive-campaign data from the GCR. * **Params**: none. * **Returns**: `result: 200` with the campaign data in `response`. ### `awardPoints` Awards incentive points to a batch of accounts. Accounts may be described as web2 social handles, cross-chain (XM) addresses, or native Demos addresses. * **Params**: `params[0]` must be an object with a `message` field holding an array of account descriptors. A missing `message` returns `result: 400` with `{ "error": "Invalid params: missing message" }`. * **Returns**: `result: 200` with `response.awardedAccounts`. Each descriptor carries a numeric `points` field plus one of the following shapes: | Descriptor | Fields | Example | | ---------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------- | | Web2 | `username`, `platform` (`twitter` \| `discord` \| `telegram` \| `github`) | `{ "username": "alice", "platform": "twitter", "points": 100 }` | | Cross-chain (XM) | `chain` (e.g. `eth.mainnet`), `address` | `{ "chain": "eth.mainnet", "address": "0xabc...", "points": 50 }` | | Native | `address` | `{ "address": "0xdef...", "points": 25 }` | ```json theme={null} { "method": "awardPoints", "params": [ { "message": [ { "username": "alice", "platform": "twitter", "points": 100 }, { "chain": "eth.mainnet", "address": "0xabc...", "points": 50 }, { "address": "0xdef...", "points": 25 } ] } ] } ``` See the [Points System](/backend/incentives/points-system) for how points and campaigns are tracked. # Storage Programs Source: https://docs.kynesys.xyz/backend/storage-programs/overview Storage Programs are first-class key–value containers that live inside the Demos Network's Global Change Registry (GCR). This document describes how t... # Storage Programs Architecture Storage Programs are first-class key–value containers that live inside the Demos Network's Global Change Registry (GCR). This document describes how the network stores, validates, and serves Storage Program data so backend operators understand the moving pieces. ## Network Responsibilities * **Deterministic provisioning** – Addresses are derived from deployer metadata, allowing replicas to agree on identifiers before the program exists on-chain. * **Consensus validation** – Create, write, access-control updates, and delete operations are executed through the transaction pipeline and require consensus signatures. * **Immediate query availability** – The query subsystem can serve reads directly from the replicated database without waiting for block production. * **Permission enforcement** – Access rules are enforced server-side during both mutation and read paths, keeping unauthorized clients from observing private data. ## Data Flow ``` ┌─────────────────────────────────────────────┐ │ Client Applications │ │ (create, write, update access, delete) │ └─────────────────┬────────────────────────────┘ │ ┌───────▼────────┐ │ Transaction │ Mutations validated by consensus │ Pipeline │───────────────────────────────┐ └───────┬────────┘ │ │ │ ▼ │ ┌────────────────────┐ │ │ Consensus Layer │ │ └───────┬────────────┘ │ │ │ ▼ │ ┌──────────────────────────────────────────────────────────┐ │ Global Change Registry (PostgreSQL) │ │ Table: gcr_storageprogram │ │ Columns: data (JSONB), acl (JSONB), metadata (JSONB) │ │ - data: user payload │ │ - acl / metadata: access control, owner, sizes, etc. │ └──────────────────────────────────────────────────────────┘ ▲ │ │ │ ┌───────┴────────┐ │ │ Query Service │────────────── RPC READ ─────────┘ └────────────────┘ ``` ## Persistence Model Each Storage Program is stored as a row in the `gcr_storageprogram` table. The user payload lives in the `data` JSONB column, access rules in the `acl` JSONB column, and network-managed fields in the `metadata` JSONB column: ```json theme={null} { "variables": { "username": "alice", "score": 1000, "settings": { "theme": "dark", "notifications": true } }, "metadata": { "programName": "myApp", "deployer": "0xdeployer123...", "accessControl": "public", "allowedAddresses": [], "created": 1706745600000, "lastModified": 1706745600000, "size": 2048 } } ``` * **variables** – Arbitrary JSON payload managed by the deployer. * **metadata** – Network-managed fields that clients can rely on for auditing and access checks. ## Deterministic Addressing Storage Program identifiers are derived on the client and verified by the network: ``` address = stor-{SHA256(deployer:programName:salt).substring(0, 40)} ``` Consensus nodes recompute the address during CREATE to guarantee uniqueness. Because the derivation is deterministic, clients can safely reference programs before they exist and the backend can reject collisions or mismatched inputs. ## Access Control Enforcement During transaction execution the validator layer enforces the configured mode: * **owner** (deployer-only) – Only the deploying address may create, read, or mutate data. * **public** – Anyone can issue read RPCs; only the deployer can commit writes. * **restricted** – Allow lists (`acl.allowed`) are stored alongside the program and validated during both read and write phases. RPC handlers consult the same metadata before serving results, ensuring confidentiality even when requests originate outside the trusted perimeter. ## Resource Limits To keep Storage Programs fast to replicate and query, the backend validates: * Maximum serialized payload of **1 MB** per program. * Maximum nesting depth of **64** JSON levels. Requests exceeding these thresholds are rejected during consensus execution before they reach the data layer. ## Related Guides * Developer-first overview: [SDK Storage Programs](/sdk/storage-programs/overview) * Implementation recipes: [Storage Program Cookbook](/sdk/cookbook/storage-programs/overview) * Client operations: [SDK Operations Guide](../../sdk/storage-programs/operations) # Validator Lifecycle Source: https://docs.kynesys.xyz/backend/validator-lifecycle/overview On-chain validator state machine: registration, top-up, unstake lock, exit. Protocol-side mechanics enforced by every node. # Validator Lifecycle A DEMOS validator is an on-chain row in the `validators` table. Its state machine is enforced by the protocol on every node — the SDK is just one of several ways to drive it. ## State machine | Status code | Status | Meaning | | ----------- | ----------- | --------------------------------------------------------- | | `"2"` | `ACTIVE` | Participating in consensus, eligible for shard selection. | | `"3"` | `UNSTAKING` | Unstake requested, lock running. Still active until exit. | | `"0"` | `EXITED` | Stake withdrawn, removed from active set. | Transitions are driven by four transaction types — each one a reflexive tx signed by the validator's own key: | From | Tx | To | Notes | | ----------- | ------------------ | ----------- | --------------------------------------------------------------------------------------------- | | (none) | `validatorStake` | `ACTIVE` | First stake; requires `staked_amount ≥ minValidatorStake` and a `connectionUrl`. | | `ACTIVE` | `validatorStake` | `ACTIVE` | Top-up — adds to existing `staked_amount`. | | `UNSTAKING` | `validatorStake` | `ACTIVE` | **Top-up resets the unstake.** `unstake_requested_at` and `unstake_available_at` are cleared. | | `ACTIVE` | `validatorUnstake` | `UNSTAKING` | Arms the lock period. Validator continues participating in consensus. | | `UNSTAKING` | `validatorExit` | `EXITED` | Only valid after the lock has elapsed. | ## Unstake lock `UNSTAKE_LOCK_BLOCKS = 1000` (≈ 10× the governance voting window). When a validator submits `validatorUnstake`, the node sets: ``` unstake_requested_at = current_block unstake_available_at = current_block + UNSTAKE_LOCK_BLOCKS ``` `validatorExit` is rejected until `current_block ≥ unstake_available_at`. The lock window is non-governable. Its purpose is to make **vote-and-run** attacks economically unattractive: a validator who votes maliciously on a governance proposal cannot exit before the proposal's tally + grace period (≈ 150 blocks) leaves time for the network to react (slashing is on the Phase-2 roadmap; the lock alone already keeps the malicious stake at risk). ## Why restake clears unstake Without this rule, an attacker could: 1. Stake → `ACTIVE` 2. Submit `unstake` → `UNSTAKING`, lock starts at block N 3. Wait 1001 blocks → lock elapsed 4. Submit `validatorStake` (top-up) → adds new stake, but status stays `UNSTAKING` with the **already-elapsed** lock 5. Immediately submit `validatorExit` → withdraws ALL stake, including the freshly-added portion that never sat under a lock By forcing the top-up to reset to `ACTIVE` and clear the unstake timestamps, the protocol guarantees every stake unit sits under a fresh 1000-block lock before exit. ## Eligibility & weight A validator's `staked_amount` is the basis for two protocol mechanics: * **Shard selection** — the consensus layer picks shard members from online validators. Stake doesn't currently weight the random pick (Phase 2), but it gates eligibility (you must be `ACTIVE`). * **Governance vote weight** — a vote's weight equals the validator's `staked_amount` at the proposal's `snapshotBlock`. The snapshot freezes weights so unstaking during the voting window does not retroactively reduce a recorded vote. ## Persistence guarantees Validator state changes ride the same atomicity rails as block insertion: * The `gcr_edit` for a staking tx is attached **before** the RPC node signs `validityData`, so it travels through DTR relay and consensus verification untouched. * `GCRValidatorStakeRoutines.apply` runs on every node when the block containing the tx is committed. The routine is idempotent — replaying the same edit yields the same result. * A failed routine throws inside `dataSource.transaction(...)` → `insertBlock` rolls the entire block back. Either every node applies the state transition or none do. ## Configuration Operators control the genesis-state minimum stake via env: | Variable | Default | | --------------------- | ------------------------------------------- | | `MIN_VALIDATOR_STAKE` | `"1000000000000000000"` (10^18, raw bigint) | Once a governance proposal activates a different `minValidatorStake`, the env value is overridden for the rest of the chain's life (until the next governance change). ## Related `stake` / `unstake` / `validatorExit` builders + read queries Stackable Genesis: how validator stake feeds vote weight # ZK API Reference Source: https://docs.kynesys.xyz/backend/zk-identity/api-reference Transaction types and data structures for ZK Identity. # API Reference ## Transaction Structure ZK Identity operations use standard `native` transactions with specific payloads. ### Identity Commitment Used to link an identity (Phase 1). ```typescript theme={null} { type: 'identity_commitment', payload: { /** Poseidon(provider_id, secret) */ commitment_hash: string, /** Provider type: 'github', 'telegram', etc. */ provider: string, /** Timestamp of creation */ timestamp: number } } ``` ### Identity Attestation Used to prove ownership without revealing identity (Phase 2). ```typescript theme={null} { type: 'identity_attestation', payload: { /** Poseidon(provider_id, secret, context) - prevents double-spending; secret enforces cross-context unlinkability */ nullifier_hash: string, /** Merkle root against which proof is verified */ merkle_root: string, /** The ZK Proof (Groth16) */ proof: { pi_a: string[], pi_b: string[][], pi_c: string[], protocol: string }, /** Public signals: [nullifier, merkle_root, context] */ public_signals: string[], /** Provider type (for categorization only) */ provider: string } } ``` ## Data Types ### IdentityProofCircuitInput **CRITICAL SECURITY**: These inputs are used locally to generate the proof and verify the `commitment`. ```typescript theme={null} interface IdentityProofCircuitInput { // Private Inputs (Witness) provider_id: string // Your provider ID secret: string // Your local secret path_elements: string[] // Merkle path siblings path_indices: number[] // Merkle path indices // Public Inputs context: string // Action context (e.g. "voting_proposal_1") merkle_root: string // Current tree root } ``` ### MerkleProofResponse Response from the node when requesting a Merkle proof for a commitment. ```typescript theme={null} interface MerkleProofResponse { proof: { siblings: string[] path_indices: number[] root: string leaf: string } leaf_index: number } ``` ## Database Schema The system uses specific tables to track commitments and nullifiers. | Table | Description | Key Fields | | ---------------------- | ------------------------------------------------- | ------------------------------- | | `identity_commitments` | Stores valid commitments | `commitment_hash`, `leaf_index` | | `used_nullifiers` | Tracks used nullifiers to prevent double-spending | `nullifier_hash` | | `merkle_tree_state` | Snapshots of the Merkle Tree | `root_hash`, `leaf_count` | # ZK Identity Overview Source: https://docs.kynesys.xyz/backend/zk-identity/overview Privacy-preserving identity attestation using zero-knowledge proofs and nullifiers. # ZK Identity System The ZK Identity System provides a privacy-preserving way to attest to an identity (e.g., verifying you are a Github user) without revealing the specific identity or linking multiple actions to the same user. It uses **Zero-Knowledge Proofs (ZK-SNARKs)** to guarantee: 1. **Privacy**: No one knows which user performed an action. 2. **Uniqueness**: A user cannot double-spend an action in the same context (via Nullifiers). 3. **Soundness**: Mathematical proof of validity. ## Core Concepts `Poseidon(provider_id, secret)` A cryptographic commitment to an identity, stored on-chain in a Merkle Tree. It hides the user's ID and secret. `Poseidon(provider_id, secret, context)` A unique hash generated for a specific action (context). Including `secret` ensures nullifiers cannot be linked across contexts even if `provider_id` leaks. Prevents double-spending while maintaining cross-context unlinkability. A valid commitment must exist in the global Merkle Tree. The ZK proof verifies inclusion in this tree. A Groth16 proof that asserts: "I know a secret corresponding to a commitment in the Merkle Tree, and here is my unique nullifier for this context." ## Architecture Overview The system operates in a **Two-Phase Flow**: ### Phase 1: Link Identity (One-time) User generates a random secret locally. This never leaves their device. User calculates commitment: `Hash(provider_id, secret)`. User submits the commitment to the blockchain via an `identity_commitment` transaction. Validators verify the transaction and add the commitment to the global Merkle Tree. ### Phase 2: Prove Ownership (Repeatable) When a user wants to perform an action (e.g., vote, claim airdrop) anonymously: User uses their secret and the current Merkle path to generate a ZK proof locally. User calculates a nullifier based on the action context: `Hash(provider_id, secret, context)`. User submits `identity_attestation` transaction with the proof and nullifier. Validators verify the proof against the current Merkle root and checking if the nullifier has been used before. ## Privacy Model | Component | Visibility | Description | | --------------- | ---------- | ------------------------------------------- | | **Provider ID** | 🔒 Private | Hidden inside commitment and ZK proof | | **User Secret** | 🔒 Private | Known only to user, never transmitted | | **Commitment** | 🌍 Public | Visible on-chain, but opaque | | **Nullifier** | 🌍 Public | Visible on-chain, used for uniqueness check | | **Context** | 🌍 Public | The specific action being performed | ## Security Guarantees * **Anonymity Set**: All users across all providers in the Merkle Tree. * **Unlinkability**: No link between the commitment (Phase 1) and the attestation (Phase 2). * **Double-Spend Protection**: The nullifier guarantees a user can only act once per context. # ZK Setup Guide Source: https://docs.kynesys.xyz/backend/zk-identity/setup How to setup and generate ZK keys for the Identity System. # Setup Instructions This guide explains how to set up the ZK circuits and keys required for the ZK Identity System. **Validators**: You must use the verification keys committed in the repository to ensure consensus compatibility. ## Prerequisites * Bun runtime * Dependencies installed (`bun install`) ## Quick Setup Run the automated setup script to download the Powers of Tau ceremony file, compile circuits, and generate keys: ```bash theme={null} bun run zk:setup ``` This process will: 1. Download Powers of Tau file (\~140MB) 2. Compile Circom circuits 3. Generate proving (`.zkey`) and verification keys ## Directory Structure The ZK files are located in `src/features/zk/`: ``` src/features/zk/ ├── circuits/ # Source .circom files ├── keys/ # Generated keys (.zkey, .json) ├── proof/ # Proof generation logic └── scripts/ # Setup scripts ``` ## Git Policy **Strict Commit Rules** * ✅ **COMMIT**: `circuits/*.circom`, `keys/verification_key.json` * ❌ **DO NOT COMMIT**: `keys/*.zkey`, `keys/*.ptau`, `*.r1cs`, `*.wasm` The `verification_key.json` is the trust anchor. All nodes must use the exact same file to verify proofs correctly. ## Manual Steps If you need to run steps manually: ### 1. Download Powers of Tau ```bash theme={null} cd src/features/zk/keys/ curl -L -o powersOfTau28_hez_final_14.ptau \ https://storage.googleapis.com/zkevm/ptau/powersOfTau28_hez_final_14.ptau ``` ### 2. Compile Circuits ```bash theme={null} bun run zk:compile ``` ### 3. Generate Keys ```bash theme={null} # Generate proving key npx snarkjs groth16 setup \ src/features/zk/circuits/identity.r1cs \ src/features/zk/keys/powersOfTau28_hez_final_14.ptau \ src/features/zk/keys/identity_0000.zkey # Export verification key npx snarkjs zkey export verificationkey \ src/features/zk/keys/identity_0000.zkey \ src/features/zk/keys/verification_key.json ``` # Node Setup Source: https://docs.kynesys.xyz/cookbook/overview Platform-specific guides for setting up and running a DEMOS Network node locally for development and testing. # Node Setup This section contains platform-specific guides for setting up and running a DEMOS Network node locally. Looking for SDK code examples? See the [Code Examples](/sdk/cookbook/overview) section in the SDK documentation. # Project Setup Source: https://docs.kynesys.xyz/cookbook/project-setup/overview Platform-specific guides for setting up and running a Demos Network node locally. # Project setup There are two supported ways to run a Demos node. Both produce a working node — they differ only in where the binary and its dependencies live. | Track | Best for | What you install on the host | | ----------------------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------- | | **Track 1: Docker Compose (recommended)** | Operators, validators, anyone who wants the fastest path to a running node | Just Docker (20.10+) and the `docker compose` v2 plugin | | **Track 2: Bare metal (`./run`)** | Core development, debugging, TUI-driven operation | Bun, Rust (for `wstcp`), plus Docker for the Postgres sidecar | If you don't have a strong reason to pick Track 2, use Track 1. The `docker compose up` workflow boots the full stack — node, Postgres, optional TLSNotary sidecar, and the Prometheus + Grafana monitoring stack — in a single command. ## Quickstart (Docker Compose) On any platform with Docker and the v2 compose plugin: ```bash theme={null} git clone https://github.com/kynesyslabs/node.git && cd node cp .env.example .env docker compose up ``` When healthy: * Node RPC: `http://localhost:53550` (try `curl http://localhost:53550/info`) * Grafana: `http://localhost:3000` (default `admin` / `demos`) * Prometheus: `http://localhost:9091` ## Platform guides Pick the guide that matches your machine. Each one walks Track 1 first and links to the canonical [`INSTALL.md`](https://github.com/kynesyslabs/node/blob/main/INSTALL.md) for the full Track 2 walkthrough. * [Run on macOS](/cookbook/project-setup/run-the-project-macos) * [Run on Ubuntu](/cookbook/project-setup/run-the-project-ubuntu) * [Run on Windows](/cookbook/project-setup/run-the-project-windows/overview) * [WSL 2 setup](/cookbook/project-setup/run-the-project-windows/wsl-2-setup) * [Issue troubleshooting](/cookbook/project-setup/run-the-project-windows/issue-troubleshooting) ## Source of truth The platform guides are kept short on purpose. For the full reference — every env var, every profile, all bare-metal options — see [`INSTALL.md`](https://github.com/kynesyslabs/node/blob/main/INSTALL.md) in the node repository and the canonical [`.env.example`](https://github.com/kynesyslabs/node/blob/main/.env.example). # Run The Project (macOS) Source: https://docs.kynesys.xyz/cookbook/project-setup/run-the-project-macos Run a Demos Network node on macOS using Docker Compose (recommended) or bare metal. # Run the project (macOS) This guide gets you to a running node on macOS. Track 1 (Docker Compose) is the recommended path; Track 2 (bare metal) is documented at the bottom for advanced setups. ## Track 1: Docker Compose (recommended) ### Prerequisites * [Docker Desktop for Mac](https://docs.docker.com/desktop/install/mac-install/) 4.x or newer (ships Docker 20.10+ and the `docker compose` v2 plugin) * `git` Verify both are present: ```bash theme={null} docker --version docker compose version ``` Use the `docker compose` form (with a space). The legacy hyphenated `docker-compose` is the deprecated Python script and is not supported. ### Three-step quickstart ```bash theme={null} git clone https://github.com/kynesyslabs/node.git && cd node cp .env.example .env docker compose up ``` The first run pulls images and builds the node container, which can take a few minutes. Subsequent starts are near-instant. When the stack is healthy: * Node RPC: `http://localhost:53550` — try `curl http://localhost:53550/info` * Grafana: `http://localhost:3000` (default `admin` / `demos`) * Prometheus: `http://localhost:9091` ### Tour of `.env` `.env.example` is the canonical template. The defaults work for local development. The variables you are most likely to touch: | Variable | Default | Notes | | ---------------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | | `CONSENSUS_TIME` | `10` | Block production interval (seconds). | | `RPC_FEE` | `1` | Per-tx fee component. Total flat fee = `RPC_FEE + NETWORK_FEE + BURN_FEE` (default `1+1+1=3`). | | `NETWORK_FEE` | `1` | Per-tx fee component. | | `BURN_FEE` | `1` | Per-tx fee component. | | `RPC_PORT` | `53550` | Host-mapped RPC port. Change if 53550 is taken. | | `EXPOSED_URL` | `http://localhost:53550` | Public URL advertised to peers. **Loopback default is fine for local dev — set it to your reachable address before joining a real network.** | | `OMNI_ENABLED` / `OMNI_PORT` | `true` / `53551` | OmniProtocol binary RPC. Modes via `OMNI_MODE`: `HTTP_ONLY`, `OMNI_PREFERRED`, `OMNI_ONLY`. | | `TLSNOTARY_ENABLED` | `true` | Set to `false` to skip TLSNotary entirely. | | `TLSNOTARY_SIGNING_KEY` | *(empty)* | **Leave empty in default docker mode** — the sidecar manages its own key. Only set when `TLSNOTARY_MODE=ffi`. | | `GRAFANA_ADMIN_PASSWORD` | `demos` | Change this. | | `COMPOSE_PROFILES` | `monitoring,tlsnotary` | Which compose profiles to bring up (see below). | Leave `PG_HOST=postgres` and `TLSNOTARY_HOST=tlsnotary` exactly as shipped — those are the in-network service names used by docker compose. Old docs referenced `RPC_FEE=5` and `SERVER_PORT`. Both are wrong. The current names are `RPC_FEE` (default `1`, plus `NETWORK_FEE=1` and `BURN_FEE=1`) and `RPC_PORT`. ### Compose profiles `COMPOSE_PROFILES` in `.env` controls the optional services: | Profile | Adds | | ------------ | ------------------------------------------------------------------ | | *(none)* | postgres + node — bare minimum | | `tlsnotary` | TLSNotary sidecar (HTTPS attestation) | | `monitoring` | Prometheus + Grafana | | `full` | node-exporter (host CPU/RAM/disk metrics) — pair with `monitoring` | | `neo4j` | Neo4j (only for CGC/KYC features) | ```bash theme={null} # Default — postgres + node + tlsnotary + monitoring (uses .env) docker compose up # Minimal — only postgres + node (also set TLSNOTARY_ENABLED=false in .env) COMPOSE_PROFILES= docker compose up # Add host-level metrics COMPOSE_PROFILES=monitoring,tlsnotary,full docker compose up ``` ### Common operations ```bash theme={null} # Follow logs docker compose logs -f node # Update to the latest source git pull docker compose up -d --build # Stop containers, KEEP volumes (your identity, chain data, etc.) docker compose down # Stop AND DELETE all volumes — nuclear, destroys identity + state docker compose down -v ``` State persists in named Docker volumes prefixed `demos_` (e.g. `demos_node_state` holds `.demos_identity`, `demos_pgdata` holds chain data, `demos_grafana_data` holds dashboards). They survive `docker compose down` but not `docker compose down -v`. ### Going public Before joining a real Demos network from a Mac that other peers should reach: 1. Set `EXPOSED_URL` in `.env` to your reachable address — public IP or DNS — not `localhost`. 2. Open inbound TCP for `53550` (RPC), `53551` (OmniProtocol), and `7047` (TLSNotary, if enabled) on your router/firewall **before** advertising the public URL. 3. Seed `demos_peerlist.json` with bootstrap peers from the team. Do not invent hostnames; use the values the team publishes. See [INSTALL.md](https://github.com/kynesyslabs/node/blob/main/INSTALL.md#joining-the-network--required-steps-before-real-world-use) for the full peerlist seeding procedure. ### Troubleshooting | Symptom | Fix | | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `docker compose: command not found` | You have the legacy Python `docker-compose`. Install the v2 plugin via Docker Desktop or [the official guide](https://docs.docker.com/compose/install/). | | Port `53550` already in use | Set `RPC_PORT` (and `EXPOSED_URL`) to a free port in `.env`, then `docker compose up -d`. | | `EXPOSED_URL` warning at startup | Expected for local dev. Set `EXPOSED_URL` to a routable address before joining a network. | | Postgres connection refused | Postgres takes a few seconds to become healthy on first boot. The node waits via `depends_on`. If it persists, check `docker compose logs postgres`. | *** ## Track 2: Bare metal with `./run` (advanced) Track 2 runs the node binary natively via Bun and uses Docker only for a Postgres sidecar managed by the `./run` script. Pick this path for core development, kernel-level debugging, or TUI-driven operation. The macOS-specific quirks (`source $HOME/.zshrc` instead of `.bashrc`, Docker Desktop instead of `apt`-installed Docker) are handled by the upstream install scripts. The full walkthrough — prerequisites, `./scripts/install-deps.sh`, identity generation, peerlist setup, and `./run` flags — lives in the source of truth: * [INSTALL.md — Track 2: Bare metal with `./run`](https://github.com/kynesyslabs/node/blob/main/INSTALL.md#track-2-bare-metal-with-run) For the bare-metal path, override these in your `.env`: * `PG_HOST=localhost` * `PG_PORT=5332` * `TLSNOTARY_HOST=localhost` # Run The Project (Ubuntu) Source: https://docs.kynesys.xyz/cookbook/project-setup/run-the-project-ubuntu Run a Demos Network node on Ubuntu using Docker Compose (recommended) or bare metal. # Run the project (Ubuntu) Ubuntu 22.04 LTS or newer is the supported and tested target. Track 1 (Docker Compose) is the recommended path; Track 2 (bare metal) is at the bottom for advanced setups. ## Track 1: Docker Compose (recommended) ### Prerequisites * Docker Engine 20.10+ and the `docker compose` v2 plugin * `git` If you do not yet have Docker installed, the steps below match the official Docker docs and `INSTALL.md`: ```bash theme={null} # Remove old Docker versions sudo apt remove docker docker-engine docker.io containerd runc # Install prerequisites sudo apt update sudo apt install -y ca-certificates curl gnupg lsb-release # Add Docker's official GPG key sudo mkdir -p /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg # Set up the repository echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" \ | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null # Install Docker Engine and the Compose v2 plugin sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin # Add yourself to the docker group so you don't need sudo sudo usermod -aG docker $USER newgrp docker ``` Verify: ```bash theme={null} docker --version docker compose version ``` Use `docker compose` (with a space). The legacy hyphenated `docker-compose` is the deprecated Python script — install the v2 plugin (`docker-compose-plugin` package) instead. ### Three-step quickstart ```bash theme={null} git clone https://github.com/kynesyslabs/node.git && cd node cp .env.example .env docker compose up ``` The first run pulls images and builds the node container, which can take a few minutes. Subsequent starts are near-instant. When the stack is healthy: * Node RPC: `http://localhost:53550` — try `curl http://localhost:53550/info` * Grafana: `http://localhost:3000` (default `admin` / `demos`) * Prometheus: `http://localhost:9091` ### Tour of `.env` `.env.example` is the canonical template. The defaults work for local development. The variables you are most likely to touch: | Variable | Default | Notes | | ---------------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | | `CONSENSUS_TIME` | `10` | Block production interval (seconds). | | `RPC_FEE` | `1` | Per-tx fee component. Total flat fee = `RPC_FEE + NETWORK_FEE + BURN_FEE` (default `1+1+1=3`). | | `NETWORK_FEE` | `1` | Per-tx fee component. | | `BURN_FEE` | `1` | Per-tx fee component. | | `RPC_PORT` | `53550` | Host-mapped RPC port. Change if 53550 is taken. | | `EXPOSED_URL` | `http://localhost:53550` | Public URL advertised to peers. **Loopback default is fine for local dev — set it to your reachable address before joining a real network.** | | `OMNI_ENABLED` / `OMNI_PORT` | `true` / `53551` | OmniProtocol binary RPC. Modes via `OMNI_MODE`: `HTTP_ONLY`, `OMNI_PREFERRED`, `OMNI_ONLY`. | | `TLSNOTARY_ENABLED` | `true` | Set to `false` to skip TLSNotary entirely. | | `TLSNOTARY_SIGNING_KEY` | *(empty)* | **Leave empty in default docker mode** — the sidecar manages its own key. Only set when `TLSNOTARY_MODE=ffi`. | | `GRAFANA_ADMIN_PASSWORD` | `demos` | Change this. | | `COMPOSE_PROFILES` | `monitoring,tlsnotary` | Which compose profiles to bring up (see below). | Leave `PG_HOST=postgres` and `TLSNOTARY_HOST=tlsnotary` exactly as shipped — those are the in-network service names used by docker compose. Old docs referenced `RPC_FEE=5` and `SERVER_PORT`. Both are wrong. The current names are `RPC_FEE` (default `1`, plus `NETWORK_FEE=1` and `BURN_FEE=1`) and `RPC_PORT`. ### Compose profiles `COMPOSE_PROFILES` in `.env` controls the optional services: | Profile | Adds | | ------------ | ------------------------------------------------------------------ | | *(none)* | postgres + node — bare minimum | | `tlsnotary` | TLSNotary sidecar (HTTPS attestation) | | `monitoring` | Prometheus + Grafana | | `full` | node-exporter (host CPU/RAM/disk metrics) — pair with `monitoring` | | `neo4j` | Neo4j (only for CGC/KYC features) | ```bash theme={null} # Default — postgres + node + tlsnotary + monitoring (uses .env) docker compose up # Minimal — only postgres + node (also set TLSNOTARY_ENABLED=false in .env) COMPOSE_PROFILES= docker compose up # Add host-level metrics COMPOSE_PROFILES=monitoring,tlsnotary,full docker compose up ``` ### Common operations ```bash theme={null} # Follow logs docker compose logs -f node # Update to the latest source git pull docker compose up -d --build # Stop containers, KEEP volumes (your identity, chain data, etc.) docker compose down # Stop AND DELETE all volumes — nuclear, destroys identity + state docker compose down -v ``` State persists in named Docker volumes prefixed `demos_` (e.g. `demos_node_state` holds `.demos_identity`, `demos_pgdata` holds chain data). They survive `docker compose down` but not `docker compose down -v`. ### Going public (VPS / public Ubuntu host) Before joining a real Demos network: 1. Set `EXPOSED_URL` in `.env` to your reachable address — public IP or DNS — not `localhost`. 2. Open the firewall **before** advertising the public URL: ```bash theme={null} sudo ufw allow 53550/tcp comment "Demos RPC" sudo ufw allow 53551/tcp comment "Demos OmniProtocol" sudo ufw allow 7047/tcp comment "Demos TLSNotary" sudo ufw enable ``` 3. Do **not** expose `5432` (Postgres), `9090` (node metrics), `9091` (Prometheus), or `3000` (Grafana) to the public internet. 4. Seed `demos_peerlist.json` with bootstrap peers from the team. Do not invent hostnames; use the values the team publishes. See [INSTALL.md](https://github.com/kynesyslabs/node/blob/main/INSTALL.md#joining-the-network--required-steps-before-real-world-use) for the full peerlist seeding procedure. ### Troubleshooting | Symptom | Fix | | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `docker compose: command not found` | You have the legacy Python `docker-compose`. Install the v2 plugin: `sudo apt install docker-compose-plugin`. | | Port `53550` already in use | Set `RPC_PORT` (and `EXPOSED_URL`) to a free port in `.env`, then `docker compose up -d`. | | Permission denied talking to Docker | `sudo usermod -aG docker $USER && newgrp docker`. | | `EXPOSED_URL` warning at startup | Expected for local dev. Set `EXPOSED_URL` to a routable address before joining a network. | | Postgres connection refused | Postgres takes a few seconds to become healthy on first boot. The node waits via `depends_on`. If it persists, check `docker compose logs postgres`. | *** ## Track 2: Bare metal with `./run` (advanced) Track 2 runs the node binary natively via Bun and uses Docker only for a Postgres sidecar managed by the `./run` script. Pick this path for core development, kernel-level debugging, or TUI-driven operation. The full walkthrough — prerequisites (`build-essential`, Bun via Mise or the direct installer, Rust for `wstcp`), `./scripts/install-deps.sh`, identity generation, `./run` flags — lives in the source of truth: * [INSTALL.md — Track 2: Bare metal with `./run`](https://github.com/kynesyslabs/node/blob/main/INSTALL.md#track-2-bare-metal-with-run) For the bare-metal path, override these in your `.env`: * `PG_HOST=localhost` * `PG_PORT=5332` * `TLSNOTARY_HOST=localhost` # Issue Troubleshooting Source: https://docs.kynesys.xyz/cookbook/project-setup/run-the-project-windows/issue-troubleshooting Common issues and fixes when running a Demos node on Windows via WSL 2 + Docker Compose, or bare metal. # Issue Troubleshooting (Windows / WSL 2) This page covers the issues you are most likely to hit on Windows. Most of them are environment-level (Docker Desktop integration, WSL 2 networking, port conflicts) rather than node-level — node logs are still the best signal once the stack is up. ## Docker Compose path (Track 1) ### `docker compose: command not found` You probably have the deprecated Python `docker-compose` (with a hyphen) installed instead of the v2 plugin (with a space). The current node tooling requires the v2 plugin. **Fix:** * On Windows, install or update [Docker Desktop](https://www.docker.com/products/docker-desktop/) — recent versions ship the v2 plugin out of the box and expose `docker compose` inside WSL 2 when WSL Integration is enabled. * Inside WSL 2, verify with: ```bash theme={null} docker compose version ``` ### `docker` works in PowerShell but not in WSL 2 Open **Docker Desktop → Settings → Resources → WSL Integration** and toggle on integration for your Ubuntu distro, then click **Apply & Restart**. Re-open the WSL shell and re-run `docker --version`. ### Port `53550` already in use Either you have another process bound to it, or a previous `docker compose` run is still up. **Fix:** edit `.env` and pick a free port: ```bash theme={null} RPC_PORT=53560 EXPOSED_URL=http://localhost:53560 ``` Then restart the stack: ```bash theme={null} docker compose down docker compose up -d ``` If you want to find what is holding the port from inside WSL: ```bash theme={null} sudo lsof -i :53550 ``` ### Postgres connection refused on first boot Postgres takes a few seconds to become healthy. The node is wired with `depends_on: condition: service_healthy`, so this normally resolves itself. If it persists: ```bash theme={null} docker compose logs postgres docker compose ps ``` If Postgres is crash-looping, the most common cause is leftover state from a prior incompatible run. Reset only the Postgres volume (this destroys chain data but keeps your identity): ```bash theme={null} docker compose down docker volume rm demos_pgdata docker compose up -d ``` ### Slow builds or strange filesystem behaviour Move the repo out of `/mnt/c/...` and into the WSL 2 home directory (e.g. `~/node`). The Windows-mounted drive is significantly slower than the native WSL 2 filesystem for builds, bind mounts, and Docker layer caching. ### `EXPOSED_URL` warning at startup `EXPOSED_URL is set to a loopback/unroutable address` is **expected** for local development. Peers will ignore your node, but RPC works locally. Set `EXPOSED_URL` to a reachable address only when you're actually exposing the node. ### Resetting everything If you want a clean slate: ```bash theme={null} docker compose down -v # destroys ALL named volumes (identity, chain data, dashboards) ``` This is irreversible. Back up `demos_node_state` first if you care about your `.demos_identity`. *** ## Bare-metal path (Track 2, inside WSL 2) The bare-metal path uses **Bun**, not Yarn or npm. If you see references to Yarn anywhere, they are stale and should be ignored. ### `bun: command not found` Install Bun inside the WSL 2 shell: ```bash theme={null} # Preferred: via Mise curl https://mise.run | sh mise use -g bun@latest # Alternate: direct installer curl -fsSL https://bun.sh/install | bash ``` Verify: ```bash theme={null} bun -v ``` ### Rebuilding dependencies from scratch If the node won't start after a `git pull` or you suspect corrupted dependencies: ```bash theme={null} rm -rf node_modules bun.lockb bun install ``` ### Postgres sidecar issues (bare-metal) The bare-metal `./run` flow manages a Postgres sidecar in `postgres_5332/`. If the database is unreachable: ```bash theme={null} # From the project root cd postgres_5332 ./clean.sh ./start.sh ``` Then go back to the project root and `./run` again. For the full bare-metal reference (every `./run` flag, the install-deps script, identity generation), see [INSTALL.md — Track 2](https://github.com/kynesyslabs/node/blob/main/INSTALL.md#track-2-bare-metal-with-run). ### Wrong env vars from old docs If you copied an older `.env` from outdated documentation, replace these: | Old (wrong) | New (correct) | | ------------------------ | ---------------------------------------------------------------------------------- | | `SERVER_PORT=53550` | `RPC_PORT=53550` | | `RPC_FEE=5` (single fee) | `RPC_FEE=1` + `NETWORK_FEE=1` + `BURN_FEE=1` (three components, default total = 3) | For the bare-metal path also set: ```env theme={null} PG_HOST=localhost PG_PORT=5332 TLSNOTARY_HOST=localhost ``` *** ## Where to look next * [Run the Project (Windows)](/cookbook/project-setup/run-the-project-windows/overview) — main setup flow * [WSL 2 Setup](/cookbook/project-setup/run-the-project-windows/wsl-2-setup) — install / verify WSL 2 * [INSTALL.md](https://github.com/kynesyslabs/node/blob/main/INSTALL.md) — canonical reference for both tracks * [.env.example](https://github.com/kynesyslabs/node/blob/main/.env.example) — every env var the node reads # Run the Project (Windows) Source: https://docs.kynesys.xyz/cookbook/project-setup/run-the-project-windows/overview Run a Demos Network node on Windows via WSL 2 + Docker Compose (recommended) or bare metal. # Run the project (Windows) The supported Windows path is **WSL 2 with Docker Compose**. Docker Desktop's WSL 2 backend integrates the `docker` and `docker compose` CLIs into your Ubuntu shell, and the node treats your WSL distro as a normal Linux host. ## Prerequisites * Windows 10 (with the WSL2 update package) or Windows 11 * WSL 2 with an Ubuntu distro — see the [WSL 2 setup guide](/cookbook/project-setup/run-the-project-windows/wsl-2-setup) * [Docker Desktop](https://www.docker.com/products/docker-desktop/) with the **WSL 2 based engine** enabled and **WSL Integration** turned on for your Ubuntu distro * `git` (already present in Ubuntu) Verify inside the Ubuntu (WSL 2) shell: ```bash theme={null} docker --version docker compose version ``` Use `docker compose` (with a space). The legacy hyphenated `docker-compose` is the deprecated Python script and is not supported. ## Track 1: Docker Compose (recommended) ### Step 1 — Enable Docker Desktop WSL 2 integration 1. Open **Docker Desktop → Settings**. 2. Under **General**, enable **Use the WSL 2 based engine**. 3. Under **Resources → WSL Integration**, enable integration and toggle on your Ubuntu distro. 4. Click **Apply & Restart**. ### Step 2 — Three-step quickstart inside WSL 2 Open the Ubuntu shell and run: ```bash theme={null} git clone https://github.com/kynesyslabs/node.git && cd node cp .env.example .env docker compose up ``` The first run pulls images and builds the node container, which can take a few minutes. Subsequent starts are near-instant. When the stack is healthy (browse from Windows; ports forward through Docker Desktop): * Node RPC: `http://localhost:53550` — try `curl http://localhost:53550/info` from the WSL shell * Grafana: `http://localhost:3000` (default `admin` / `demos`) * Prometheus: `http://localhost:9091` Clone the repo **inside the WSL 2 filesystem** (e.g. `~/node`), not under `/mnt/c/...`. WSL 2's Linux filesystem is dramatically faster for builds and Docker bind mounts than the Windows-mounted drive. ### Step 3 — Tour of `.env` `.env.example` is the canonical template. The defaults work for local development. The variables you are most likely to touch: | Variable | Default | Notes | | ---------------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | | `CONSENSUS_TIME` | `10` | Block production interval (seconds). | | `RPC_FEE` | `1` | Per-tx fee component. Total flat fee = `RPC_FEE + NETWORK_FEE + BURN_FEE` (default `1+1+1=3`). | | `NETWORK_FEE` | `1` | Per-tx fee component. | | `BURN_FEE` | `1` | Per-tx fee component. | | `RPC_PORT` | `53550` | Host-mapped RPC port. Change if 53550 is taken. | | `EXPOSED_URL` | `http://localhost:53550` | Public URL advertised to peers. **Loopback default is fine for local dev — set it to your reachable address before joining a real network.** | | `OMNI_ENABLED` / `OMNI_PORT` | `true` / `53551` | OmniProtocol binary RPC. Modes via `OMNI_MODE`: `HTTP_ONLY`, `OMNI_PREFERRED`, `OMNI_ONLY`. | | `TLSNOTARY_ENABLED` | `true` | Set to `false` to skip TLSNotary entirely. | | `TLSNOTARY_SIGNING_KEY` | *(empty)* | **Leave empty in default docker mode** — the sidecar manages its own key. Only set when `TLSNOTARY_MODE=ffi`. | | `GRAFANA_ADMIN_PASSWORD` | `demos` | Change this. | | `COMPOSE_PROFILES` | `monitoring,tlsnotary` | Which compose profiles to bring up. | Leave `PG_HOST=postgres` and `TLSNOTARY_HOST=tlsnotary` exactly as shipped — those are the in-network service names used by docker compose. Old docs referenced `RPC_FEE=5` and `SERVER_PORT`. Both are wrong. The current names are `RPC_FEE` (default `1`, plus `NETWORK_FEE=1` and `BURN_FEE=1`) and `RPC_PORT`. ### Common operations (run from the WSL shell) ```bash theme={null} # Follow logs docker compose logs -f node # Update to the latest source git pull docker compose up -d --build # Stop containers, KEEP volumes (your identity, chain data, etc.) docker compose down # Stop AND DELETE all volumes — nuclear, destroys identity + state docker compose down -v ``` State persists in named Docker volumes prefixed `demos_` (e.g. `demos_node_state` holds `.demos_identity`). Docker Desktop manages the volumes — they survive `docker compose down` but not `docker compose down -v`. ### Going public If you want peers to reach a node hosted on a Windows machine, you'll need to forward the relevant inbound TCP ports through both the Windows Defender Firewall and your router (and set `EXPOSED_URL` to your public address) **before** advertising the URL: * `53550` — Node RPC * `53551` — OmniProtocol * `7047` — TLSNotary (only if enabled) For production deployments, a Linux VPS is strongly preferred over a desktop Windows host. ### Troubleshooting If you hit an issue, see the dedicated [Issue Troubleshooting](/cookbook/project-setup/run-the-project-windows/issue-troubleshooting) page. *** ## Track 2: Bare metal with `./run` (advanced) Track 2 runs the node binary natively via Bun inside WSL 2 and uses Docker only for a Postgres sidecar managed by the `./run` script. Pick this path only for core development, kernel-level debugging, or TUI-driven operation. **Do not run Track 2 from Windows directly** — always use the Ubuntu shell inside WSL 2. The full walkthrough — Bun via Mise (or the direct installer), Rust for `wstcp`, `./scripts/install-deps.sh`, `./run` flags — lives in the source of truth: * [INSTALL.md — Track 2: Bare metal with `./run`](https://github.com/kynesyslabs/node/blob/main/INSTALL.md#track-2-bare-metal-with-run) For the bare-metal path, override these in your `.env`: * `PG_HOST=localhost` * `PG_PORT=5332` * `TLSNOTARY_HOST=localhost` # WSL 2 Setup Source: https://docs.kynesys.xyz/cookbook/project-setup/run-the-project-windows/wsl-2-setup Install and configure WSL 2 on Windows 10 and 11 — the recommended host for both Docker Compose and bare-metal Demos node setups. # WSL 2 Setup on Windows (10 and 11 only) WSL 2 is the recommended host for **both** the Docker Compose path and the bare-metal `./run` path on Windows. Install it once and use the same Ubuntu distro for whichever track you pick on the [Run the Project (Windows)](/cookbook/project-setup/run-the-project-windows/overview) page. 1. Run the Windows **PowerShell as Administrator** 2. Enable **WSL** by running the command: `dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart` 3. Enable the Virtual Machine Platform by running the following command: `dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart` 4. Restart the computer to apply the changes 5. Install the **WSL2** Update package (**Note: If you are using Windows 11 skip this step**) 1. Download the **WSL2** [Linux kernel update](https://learn.microsoft.com/en-us/windows/wsl/install-manual) package. 2. Run the installer and follow the on-screen instructions. 6. Open **PowerShell as Administrator** again 7. Run the following command to set **WSL2** as the default version: `wsl --set-default-version 2` 8. Install [Ubuntu](https://apps.microsoft.com/detail/9pdxgncfsczv?hl=en-us\&gl=US) from the Microsoft Store. 9. Click Install and follow the on-screen instructions. (**Note: Make sure to keep the set password**) 10. You're all set, to make sure that you are successfully done you can run the following command: `wsl --list --verbose` You should see this ![](https://lh7-rt.googleusercontent.com/docsz/AD_4nXd8XCoT-kBT_xlw4fX2axwSoS1QWkIhk2NOMH7Ms5-XRLX62XFk1B_efGlQVDyLwTJGKw19yrlUstM-1OI0EzZPJWL-PRM19SLUVeK1303T4kIAha2u-9dgcs4MfPPo_TQBE0pU9Q?key=RkbH77fVOlsq6AUc04iv4DYy) ## Next step Continue with the [Run the Project (Windows)](/cookbook/project-setup/run-the-project-windows/overview) guide. The Track 1 (Docker Compose) flow is the recommended path; once Docker Desktop is configured to use the WSL 2 backend with integration enabled for your Ubuntu distro, the standard `docker compose up` workflow runs identically to a native Linux host. # FAQ Source: https://docs.kynesys.xyz/faq Frequently asked questions about the Demos Network, SDK, and getting started. This page addresses the most commonly asked questions about the current status of the Demos Network. ## What You Can Do #### How is Demos being rolled out? Demos is being rolled out in 4 phases with a number of releases per phase. You will find the most recent release note in the release note section. For an outline of the 4 phases please review our [Road to the Omniweb](https://mirror.xyz/0x6C1c06BCBb5BB1Ebd3790c11069Dbd5712314512/xA4F_WBy17mjX0PEMh1y4vf1tz8xdTlvNU4j_ZqgEiE) article #### How do I check the network status? You can check the network status by heading to the [status page](https://status.demos.sh/). You can view block and transaction data on our [explorer](https://explorer.demos.sh). #### How can I create a Demos wallet? TBD #### How do I download the SDK? We recommend using [bun](https://bun.sh) as your package manager. All SDK examples are tested with bun. ```bash theme={null} bun add @kynesyslabs/demosdk@latest ``` While `npm` and `yarn` are also supported, we strongly recommend using bun for the best development experience. #### How do I download the node software? The node software is available in the [node repository](https://github.com/kynesyslabs/node). The recommended way to run a node is via Docker Compose: ```bash theme={null} git clone https://github.com/kynesyslabs/node.git && cd node cp .env.example .env docker compose up ``` See the [Project Setup](/cookbook/project-setup/overview) guides and the repository's `INSTALL.md` for the full walkthrough (Docker Compose and bare-metal tracks). #### How can I become a validator? You can become a validator by running the node software and following the install instructions. Staking will be required in a later phase, initially no staking is required to enable faster onboarding and testing. #### What are the requirements for running a Demos node? To run a Demos node the minimum requirements are: * 4GB RAM * 4 CPU cores (2GHz or higher) * Modern SSD storage * 200 Mbps internet connection * Ubuntu 22.04 LTS or a compatible Linux distribution The recommended specifications are 8GB RAM or higher, 6 CPU cores, high-performance SSD storage, and a 1 Gbps internet connection. #### How can I make a transaction? You can refer to the [Transactions](/sdk/websdk/transactions/overview) section on how to build transactions. At this stage you cannot forward transactions to public nodes, you are able to use the SDK locally and use the very same code once public RPCs are available. #### Is there a Faucet? How can I get DEM tokens? You can request Demos tokens at the [Demos Faucet](https://faucet.demos.sh/). ## What You Cannot Do (Yet) * XM and Web2 transactions are brittle as they are being constantly upgraded and improved # Demos Providers Discovery Mechanism Source: https://docs.kynesys.xyz/frontend/demos-providers-discovery-mechanism This mechanism allows for the discovery of multiple providers without race conditions. The injected providers communicate with the DApp through window... # Demos Providers Discovery Mechanism ## Mechanism This mechanism allows for the discovery of multiple providers without race conditions. The injected providers communicate with the DApp through window events. When the DApp needs the list of available providers, it sends a "request" event. When a request is triggered, the providers must send an "announce" event. It follows that the DApp and the providers must be listening for, respectively, "announce" and "request" events. ## Request ```typescript theme={null} interface DemosRequestProviderEvent extends Event{ type:"demosRequestProvider", } ``` ## Announce ```typescript theme={null} interface DemosAnnounceProviderEvent extends CustomEvent{ type:"demosAnnounceProvider", detail: DemosProviderDetail; } ``` ### DemosProviderDetail ```typescript theme={null} interface DemosProviderDetail { info: DemosProviderInfo; provider: DemosProvider; } ``` ### DemosProviderInfo ```typescript theme={null} export default interface DemosProviderInfo { uuid: string; name: string; icon: string; url: string; } ``` # Demos Wallet Provider Api Source: https://docs.kynesys.xyz/frontend/demos-wallet-provider-api/overview The injected provider exposes a small surface: # Demos Wallet Provider API ### Provider Interface The injected provider exposes a small surface: * `provider.request({ method, params? })`: Generic RPC-style method. Preferred for all interactions. * `provider.accounts() → Promise<{ code: 404|401|200, data: string|null }>`: Returns account status/address, used primarily for discovery gating. The provider posts messages into the page. Internally, they are forwarded to the extension background and resolved back into a unified response: ```typescript theme={null} export default interface DemosProviderResponse { id: string; type: "demosProviderResponse"; method: string; // echoes requested method success: boolean; data?: any; error?: { code: string; message: string; details?: any; } } ``` ### Supported Methods See all supported methods here: [wallet-provider-api-methods.md](/frontend/demos-wallet-provider-api/wallet-provider-api-methods) ### Usage Examples Connect and get xM addresses: ```javascript theme={null} window.dispatchEvent(new Event('demosRequestProvider')); window.addEventListener('demosAnnounceProvider', async (evt) => { const provider = evt.detail.provider; // Optional: discover account status const accounts = await provider.accounts(); if (accounts.code !== 200) { const res = await provider.request({ method: 'connect' }); if (!res.success) throw new Error(res.error?.message ?? 'Connect failed'); } const result = await provider.request({ method: 'getXmIdentities' }); console.log(result.success ? result.data : result.error); }); ``` Send a transaction: ```javascript theme={null} const tx = await provider.request({ method: 'nativeTransfer', params: [{ amount: 1, recipientAddress:"0x..." }], }); if (!tx.success) { // handle tx.error.code/message } ``` Add a Twitter identity (shape defined by SDK `TwitterProof`): ```javascript theme={null} const proof = /* obtain TwitterProof via your backend/SDK */ const res = await provider.request({ method: 'addTwitterIdentity', params: [proof] }); ``` ### Error Semantics Common error codes surfaced in `response.error.code`: * `TIMEOUT`: popup not resolved before timeout * `POPUP_ERROR`: could not open popup * `...Cancelled`: user closed popup (`validationCancelled`, `addTwitterIdentityCancelled`, etc.) * Method-specific errors (e.g., `addGithubIdentityError`, `sendTransactionError`) include `details` when available ### Notes * The extension only announces the provider after `provider.accounts()` indicates a non-404 status. * Always prefer `provider.request` with the methods above for forward-compatibility. # Wallet Provider Api Methods Source: https://docs.kynesys.xyz/frontend/demos-wallet-provider-api/wallet-provider-api-methods Call each via `provider.request({ method: '', params: [...] })` unless noted otherwise. # Wallet Provider API methods Call each via `provider.request({ method: '', params: [...] })` unless noted otherwise. #### Common error codes * **TIMEOUT**: popup not resolved before timeout * **POPUP\_ERROR**: could not open popup * **...Cancelled**: user closed popup (e.g., `validationCancelled`, `addTwitterIdentityCancelled`) * Method-specific errors (e.g., `nativeTransferError`, `sendTransactionError`), with optional `details` ## Connection #### connect * **Params**: none * **Behavior**: opens a connection popup; user approves/denies. * **Success data**: `{ address: string }` * **Errors**: `connectionDenied`, `connectionCancelled`, `TIMEOUT`, `POPUP_ERROR` Example: ```ts theme={null} const res = await provider.request({ method: 'connect' }); if (res.success) { console.log('Connected address:', res.data.address); } ``` ## Transactions These let your dApp construct transactions (or messages) and have the wallet sign and/or broadcast them. #### sign * **Params**: `[ message: string ]` * **Behavior**: opens a sign popup for a plain message. * **Success data**: `{ signature: string }` (wrapped by the wallet; underlying object includes `{ success: true, signature }`) * **Errors**: `signError`, `signCancelled`, `TIMEOUT`, `NOT_LOGGED_IN` Example: ```ts theme={null} const res = await provider.request({ method: 'sign', params: ['hello world'] }); ``` #### signTransaction * **Params**: `[ unsignedTransaction: Transaction ]` * **Behavior**: opens a sign-transaction popup and returns the signed transaction. * **Success data**: `{ signedTransaction }` (wrapped; underlying object includes `{ success: true, signedTransaction }`) * **Errors**: `signTransactionError`, `signTransactionCancelled`, `TIMEOUT`, `NOT_LOGGED_IN` Example: ```ts theme={null} const unsignedTx = {/* Transaction from SDK */}; const res = await provider.request({ method: 'signTransaction', params: [unsignedTx] }); ``` #### sendTransaction * **Params**: `[ transaction: Transaction ]` * **Behavior**: signs the transaction, validates it, opens validation popup; on confirmation, broadcasts. * **Success data**: `{ result, validityData }` * **Errors**: `sendTransactionError`, `sendTransactionCancelled`, `TIMEOUT`, `POPUP_ERROR` Example: ```ts theme={null} const tx = {/* Transaction from SDK */}; const res = await provider.request({ method: 'sendTransaction', params: [tx] }); ``` #### sendSignedTransaction * **Params**: `[ signedTransaction: Transaction ]` * **Behavior**: validates the signed transaction, opens validation popup; on confirmation, broadcasts. * **Success data**: `{ result, validityData }` * **Errors**: `sendSignedTransactionError`, `sendSignedTransactionCancelled`, `TIMEOUT`, `POPUP_ERROR` Example: ```ts theme={null} const signedTx = {/* signed Transaction from SDK */}; const res = await provider.request({ method: 'sendSignedTransaction', params: [signedTx] }); ``` ## Transaction helpers These will construct, validate and broadcast transactions directly inside the wallet by passing the required parameters. #### nativeTransfer * **Params**: `[ { recipientAddress: string, amount: number } ]` * **Behavior**: creates a transfer, validates it, then opens validation popup for user confirmation and broadcast. * **Success data**: `{ result, validityData }` * **Errors**: `nativeTransferError`, `nativeTransferCancelled`, `TIMEOUT`, `POPUP_ERROR` Example: ```ts theme={null} const res = await provider.request({ method: 'nativeTransfer', params: [{ recipientAddress: '0xabc...', amount: 1 }], }); ``` ### Identity #### getXmIdentities * **Params**: none * **Success data**: list of identities from SDK * **Errors**: `getXmIdentitiesError` Example: ```ts theme={null} const res = await provider.request({ method: 'getXmIdentities' }); ``` #### addXmIdentity * **Params**: `[payload: InferFromSignaturePayload]` * **Behavior**: infers and validates identity, then prompts user in validation popup; upon confirmation broadcasts. * **Success data**: `{ result, validityData }` * **Errors**: `addXmIdentityError`, `addXmIdentityCancelled`, `TIMEOUT` Example: ```ts theme={null} const payload = /* build InferFromSignaturePayload */ const res = await provider.request({ method: 'addXmIdentity', params: [payload] }); ``` #### removeXmIdentity * **Params**: `[payload: XMCoreTargetIdentityPayload]` * **Behavior**: creates removal validity data and prompts user in validation popup; upon confirmation broadcasts. * **Success data**: `{ result, validityData }` * **Errors**: `removeXmIdentityError`, `removeXmIdentityCancelled`, `TIMEOUT` Example: ```ts theme={null} const payload = { /* XMCoreTargetIdentityPayload */ }; const res = await provider.request({ method: 'removeXmIdentity', params: [payload] }); ``` #### getWeb2Identities * **Params**: none * **Success data**: identities list from SDK * **Errors**: `getWeb2IdentitiesError` Example: ```ts theme={null} const res = await provider.request({ method: 'getWeb2Identities' }); ``` #### getWeb2IdentityProofPayload * **Params**: none * **Success data**: proof payload to be used for Web2 identity linking * **Errors**: `getWeb2IdentityProofPayloadError` (also returned if user not logged in) Example: ```ts theme={null} const res = await provider.request({ method: 'getWeb2IdentityProofPayload' }); ``` #### addTwitterIdentity * **Params**: `[payload: TwitterProof]` * **Behavior**: validates and prompts user in validation popup; upon confirmation broadcasts. * **Success data**: `{ result, validityData }` * **Errors**: `addTwitterIdentityError`, `addTwitterIdentityCancelled`, `TIMEOUT` Example: ```ts theme={null} const proof = /* TwitterProof from your backend/SDK */ const res = await provider.request({ method: 'addTwitterIdentity', params: [proof] }); ``` #### removeWeb2Identity * **Params**: `[ { context: string, username: string } ]` * **Behavior**: constructs validity data and prompts user in validation popup; upon confirmation broadcasts. * **Success data**: `{ result, validityData }` * **Errors**: `removeWeb2IdentityError`, `removeWeb2IdentityCancelled`, `TIMEOUT` Example: ```ts theme={null} const res = await provider.request({ method: 'removeWeb2Identity', params: [{ context: 'twitter', username: 'handle' }], }); ``` # Demos Network Architecture Source: https://docs.kynesys.xyz/introduction/demos-network-architecture The Demos Network primarily consists of two main parts, divided into the repositories `sdks` and `node`. The Demos Network primarily consists of two main parts, divided into the repositories `sdks` and `node`. The **`sdks`** repository includes types, methods, and interfaces used in both frontend and backend. It contains most of what you need to interact with the Demos Network from a dApp, plus the building blocks for a custom client. See the [SDK Getting Started](/sdk/getting-started) guide for an introduction, or the [WebSDK overview](/sdk/websdk/overview) for the main client surface. The **`node`** repository contains the official implementation of Demos Network specifications for a backend client (commonly known as a node). Nodes form the core of the network and can act either as validators or as synced read-only nodes. For an in-depth tour of node-side mechanisms, start with [Internal Mechanisms](/backend/internal-mechanisms/overview), [Consensus Mechanism](/backend/consensus-mechanism/overview), and the [Global Change Registry](/backend/global-change-registry/overview). # Introduction Source: https://docs.kynesys.xyz/introduction/overview Welcome to the Demos Network technical documentation - a borderless interconnectivity layer enabling seamless interoperability across various chains and web contexts. **Important Notice** This documentation outlines the Demos Network specifications. It may contain areas that are still under development. For the most accurate information, refer to specific sections for guidance on what's available versus what remains in progress. Find answers in the [FAQ](/faq) section or ask in our [Discord](https://discord.gg/SdRqbKEcEJ). Report discrepancies or issues to [team@kynesys.xyz](mailto:team@kynesys.xyz). Welcome to the **Demos Network** technical documentation! This documentation is intended for developers and technical audiences who want to build on or integrate with the Demos Network. The Demos Network is continually expanding and improving. Check back regularly for updates and new features. ## Quick Links Learn about the core concepts and architecture Install and start building with the Demos SDK Common questions and answers Complete API documentation # Quick Start Source: https://docs.kynesys.xyz/introduction/quick-start Get up and running with the DEMOS SDK in under 5 minutes. This tutorial walks you through installation, wallet setup, and your first blockchain queries. # Quick Start This guide will have you connected to the DEMOS Network and querying the blockchain in just a few minutes. Add the DEMOS SDK to your project using bun (recommended): ```bash theme={null} bun add @kynesyslabs/demosdk@latest ``` For browser environments, you'll need Buffer polyfills. See [Getting Started](/sdk/getting-started) for Vite configuration. Generate a new wallet with a mnemonic seed phrase: ```typescript theme={null} import { Demos } from "@kynesyslabs/demosdk/websdk" const demos = new Demos() // Generate a new mnemonic (save this securely!) const mnemonic = demos.newMnemonic() console.log("Your mnemonic:", mnemonic) // Connect using the mnemonic const publicKey = await demos.connectWallet(mnemonic, { algorithm: "ed25519", }) console.log("Your public key:", publicKey) ``` **Security**: Never share your mnemonic. Store it securely - it controls access to all your keys. Connect to a DEMOS RPC node: ```typescript theme={null} await demos.connect("https://node2.demos.sh") if (demos.connected) { console.log("Connected to DEMOS Network!") } ``` Now you can read data from the blockchain: ```typescript theme={null} // Get the latest block number const blockNumber = await demos.getLastBlockNumber() console.log("Latest block:", blockNumber) // Get the latest block hash const blockHash = await demos.getLastBlockHash() console.log("Latest block hash:", blockHash) // Get your wallet info const address = demos.getAddress() const addressInfo = await demos.getAddressInfo(address) console.log("Your wallet info:", addressInfo) ``` Test your wallet by signing and verifying a message: ```typescript theme={null} const message = "Hello DEMOS Network!" // Sign the message const signature = await demos.signMessage(message) console.log("Signature:", signature) // Verify the signature const publicKey = demos.getAddress() const isValid = await demos.verifyMessage( message, signature.data, publicKey, { algorithm: signature.type } ) console.log("Signature valid:", isValid) ``` ## Complete Example Here's the full code to copy and run: ```typescript theme={null} import { Demos } from "@kynesyslabs/demosdk/websdk" async function main() { // Initialize const demos = new Demos() // Create wallet const mnemonic = demos.newMnemonic() console.log("Mnemonic:", mnemonic) const publicKey = await demos.connectWallet(mnemonic, { algorithm: "ed25519", }) console.log("Public key:", publicKey) // Connect to network await demos.connect("https://node2.demos.sh") console.log("Connected:", demos.connected) // Query blockchain const blockNumber = await demos.getLastBlockNumber() console.log("Latest block:", blockNumber) // Sign message const signature = await demos.signMessage("Hello DEMOS!") console.log("Signed message successfully!") // Clean up demos.disconnect() } main().catch(console.error) ``` ## Next Steps Learn how to create and broadcast transactions Execute transactions on other blockchains via DEMOS Explore all available blockchain queries Learn about PQC algorithms and advanced key management # What Is Demos Network Source: https://docs.kynesys.xyz/introduction/what-is-demos-network The Demos Network represents a groundbreaking advancement in blockchain technology, acting as a borderless interconnectivity layer that enables seamle... # What is Demos Network The Demos Network represents a groundbreaking advancement in blockchain technology, acting as a borderless interconnectivity layer that enables seamless interoperability and advanced functionalities across various chains and web contexts. Designed with an intent-based framework, Demos facilitates decentralized communication and interaction among Web2, Web3, and blockchain networks, addressing the limitations of isolation and incompatibility. Demos's architecture incorporates innovative features such as Game Theory Incentives (GTI), Dynamic Validation Shard Size, and advanced consensus mechanisms like Proof-of-Representation with Byzantine Fault Tolerance (PoR-BFT). These features ensure scalability, efficiency, and security while fostering a truly decentralized ecosystem. By integrating Web2 resources, cross-chain logic, and privacy-centric solutions like Zero Knowledge Proofs, Demos aims to create a robust, scalable, and adaptable network for diverse applications. **The following chapter will introduce you to the primary components that make up the Demos Network's specifications and implementation.** ## Next Steps Explore the technical architecture of Demos Network Start building with the DEMOS SDK # Bridges Source: https://docs.kynesys.xyz/sdk/bridges/overview Cross-chain bridge integrations for transferring assets between blockchains. # Bridges Cross-chain bridges enable seamless asset transfers between different blockchains through the DEMOS Network. For backend implementation details and architecture, see [Backend Bridges](/backend/bridges/overview). ## Available Bridges Multi-chain DEX aggregator integration for cross-chain swaps # Rubic Bridge Test Source: https://docs.kynesys.xyz/sdk/bridges/rubic-bridge-test Run the Rubic bridge integration tests in `rubic.test.ts`. This page covers the integration tests for the `RubicBridge` SDK class, located at `/src/tests/bridge/rubic.test.ts`. The test file exercises three flows: `getTrade`, `executeTrade` with mock data, and `executeTrade` with real funds. Before running the tests, start the `node` repository so the bridge RPC is reachable. The first two tests (`getTrade` and the mock `executeTrade`) work without any wallet setup — the default test mnemonic is fine. To run `executeTrade` with real funds: 1. Remove the `skip` operator from the test (set it to `only` if you want to run it in isolation). 2. Replace the test mnemonic in `rubic.test.ts` with a wallet that holds funds on the source chain. Run the suite: ```bash theme={null} bun test src/tests/bridge/rubic.test.ts ``` ### `getTrade` Fetches the best cross-chain trade option for the supplied payload. Returns the best trade or an error if no valid route is found. ### `executeTrade` Submits the trade. Manages allowance, approval, the swap call, and confirmation. Returns the transaction receipt. ### Payload shape ```ts theme={null} import { BridgeTradePayload, SupportedChains, } from "@kynesyslabs/demosdk/bridge" const payload: BridgeTradePayload = { fromToken: "USDT", toToken: "USDT", amount: 1, fromChainId: 137, toChainId: 1, } ``` `fromToken` / `toToken` accept `"NATIVE" | "USDC" | "USDT"`. See the [crosschain swap recipe](/sdk/cookbook/swap/crosschain-swap) for the production-flow example. # Changelog Source: https://docs.kynesys.xyz/sdk/changelog Track the latest updates, improvements, and new features in the DEMOS SDK. # Changelog ## v3.1.0 *May 7, 2026* Polish release on top of v3.0.0. Bug fix for serializer key ordering and JSDoc/type accuracy improvements. ### Fixes * `fix(serializer)`: preserve `transaction_fee` source key order via spread, ensuring stable wire serialization. * `docs+types`: JSDoc accuracy fix, return type fix, and stale comment cleanup across the public API surface. *** ## v3.0.0 *May 7, 2026* Major release. Introduces sub-DEM denomination via the `osDenomination` fork. `1 DEM = 10^9 OS`, exposed through a new `denomination` module and `bigint`-aware public API. See [Amounts & Denominations](/sdk/websdk/transactions/denominations) for the full developer guide. v3.0.0 was first published as `3.0.0-rc.1` on May 7, 2026 and promoted to `3.0.0` the same release window. The two share the same feature set; v3.1.0 then layered on the serializer fix above. ### New `denomination` module Exports: `demToOs`, `osToDem`, `parseOsString`, `toOsString`, `formatDem`, `MIN_AMOUNT_OS`, `ZERO_OS`, plus constants `OS_DECIMALS = 9` and `OS_PER_DEM = 10n ** 9n`. ```ts theme={null} import { denomination } from "@kynesyslabs/demosdk" const amountOs = denomination.demToOs("1.5") // 1_500_000_000n const amountDem = denomination.osToDem(amountOs) // "1.5" ``` ### Public API widened to `bigint` `Demos.transfer` and `Demos.pay` now accept `number | bigint`. `bigint` (OS) is the preferred form going forward — `number` (whole DEM) continues to work but is deprecated and will be removed in v4. ```ts theme={null} // preferred await demos.transfer(to, 1_500_000_000n) // 1.5 DEM in OS // still accepted, deprecated await demos.transfer(to, 1) // 1 DEM ``` ### Fork detection: `Demos.getNetworkInfo()` New RPC `Demos.getNetworkInfo(): Promise` reports whether the connected node is post-fork. Results are cached per RPC URL with a 30-second failure-TTL recovery window so transient errors don't pin a node to "unknown" forever. ### `SubDemPrecisionError` Thrown when a caller submits sub-DEM precision against a pre-fork node, where the value would otherwise silently truncate. Carries `amountOs` and `subDemRemainderOs` for diagnostics. ### Wire format widening `TransactionContent.amount`, `TxFee.*`, `RawTransaction.{amount,networkFee,rpcFee,additionalFee}`, and `StatusNative.balance` now accept `number | string`. The SDK normalizes everything to `bigint` internally. ### Dual-format `serializerGate` The serializer emits the wire shape that matches the cached fork status of the target node, so the same client transparently talks to pre-fork and post-fork validators. ### Bug fixes carried in v3.0 * `STORAGE_PROGRAM_CONSTANTS.FEE_PER_CHUNK` corrected from `1n` to `OS_PER_DEM`. Pre-v3.0 this was a 10⁹× under-billing of storage chunk fees. * `getAddressInfo(...).balance` now returns `bigint` OS on post-fork nodes. *** ## v2.12.2 *May 6, 2026* Validator staking, on-chain governance, and a TS strict-mode fix. Brings PR #84 (`feat-upgradable-network`) into a minor release. See [Validator Staking](/sdk/websdk/validator-staking/overview) and [Governance](/sdk/websdk/governance/overview). ### Validator staking * New `DemosTransactions.stake(amount: string, connectionUrl: string, demos)` — bigint-encoded amount string in OS; `connectionUrl` is required on first stake. * New `DemosTransactions.unstake(demos)` and `DemosTransactions.claimUnstakedAmount(demos)`. * New read methods on `Demos`: `getValidatorInfo(address)`, `getValidators(blockNumber?)`, `getStakedAmount(address)`. ### On-chain governance * New `DemosTransactions.proposeNetworkUpgrade(params, demos)` — `params.proposalId` (UUID), `params.proposedParameters: Partial`, `params.rationale` (≤ 1024 bytes), `params.effectiveAtBlock`. Only active validators may propose. * New `DemosTransactions.voteOnUpgrade(proposalId, approve, demos)` — one vote per proposal; voter must be in the snapshot validator set. * New read methods on `Demos`: `getNetworkParameters()`, `getActiveProposals()`, `getProposalVotes(id)`, `getUpgradeHistory()`. ### Other * Replace `Math.random()` with `crypto.getRandomValues()` for secure random generation across multiple modules. * Validate network parameters before caching (`_getNetworkParametersCached`). * Simplify `stake()` validation; add JSDoc to internal helpers. * Preserve `Partial` typing in `proposeNetworkUpgrade`. * Remove invalid `override` modifier on the `cause` property in error classes (TS strict-mode fix). *** ## v2.12.1 *May 5, 2026* Transport-layer hardening and deterministic broadcast helpers. See [Broadcasting a Transaction](/sdk/websdk/transactions/broadcasting-a-transaction#error-types). ### `broadcastAndWait` * New `Demos.broadcastAndWait(validityData, opts?)` — deterministic tx-inclusion polling. Throws `BroadcastTimeoutError` on timeout. Returns `{ broadcast, status: { state, blockNumber? } }`. * Lower-level `DemosTransactions.broadcastAndWait` adds an opt-in `failFastOnBroadcastError` flag that throws `BroadcastFailedError` immediately when the broadcast can't reach the node (`ECONNREFUSED`, `ENOTFOUND`). HTTP 5xx is **not** treated as fail-fast — the server did answer. ### Transport layer * HTTP `Retry-After` header support (seconds or HTTP-date). * `MAX_BACKOFF_MS = 8000` cap on exponential backoff so a misbehaving node can't stall a single call indefinitely. * Accurate attempt count in `TransportError`. * Stop forwarding `rpcCall`'s `retries` argument into the `_doPost` transport budget — retry budgets no longer multiply across layers. ### New error class * `TransportError` (in `@kynesyslabs/demosdk/websdk`) carrying `attempts` and `cause`. *** ## v2.12.0 *May 1, 2026* Adds the foundational denomination utilities that v3.0.0 builds on, plus storage-program test cleanup. * New `denomination` module with DEM/OS conversion utilities (consumed publicly starting in v3.0.0). * Updated storage program test names for clarity. *** ## v2.11.6 *April 20, 2026* * Add `sign` and `read` methods to `demos.storagePrograms` for streamlined storage-program transaction construction. *** ## v2.11.5 *March 26, 2026* L2PS messaging client and protocol types (PR #82, `feat/l2ps-messaging-types`). * New `L2PS` class exported from `@kynesyslabs/demosdk/l2ps`. * Factory: `L2PS.create(privateKey?, iv?)`. * Registry: `getInstance`, `getInstances`, `hasInstance`, `removeInstance`. * Instance methods: * `encryptTx(tx, senderIdentity?)` — wraps a transaction in `L2PSEncryptedPayload`. The wrapped tx type becomes `l2psEncryptedTx`. * `decryptTx(encryptedTx)` — recovers the inner transaction. *** ## v2.11.4 *March 16, 2026* * `fix(abstraction)`: export `IdentityAttestationPayload` and `IdentityCommitmentPayload` from the `/abstraction` barrel. *** ## v2.11.3 *March 16, 2026* Multi-instance crypto isolation. * Per-`Demos`-instance `_cryptoInstanceId` (UUID) — fixes identity bleed across multiple `Demos` instances in the same process. Previously the encryption singleton could leak state between independently-constructed clients. * Additional small bugfixes around the encryption singleton. *** ## v2.11.2 *March 4, 2026* Human Passport identity integration (PR #73). See [Human Passport identity](/sdk/web2/identities/human-passport). * `Identities.getHumanPassportScore(demos, address)` * `Identities.getHumanPassportIdentities(demos, demosAddress)` * `Identities.addHumanPassportIdentity(demos, payload)` — supports `api` and `onchain` verification methods. Default score threshold is 20. Throws if `verificationMethod === "onchain"` and `chainId == null`. * `Identities.removeHumanPassportIdentity(demos, address)` *** ## v2.11.1 *March 3, 2026* Ethos identity integration (PR #74). See [Ethos identity](/sdk/web2/identities/ethos). * `Identities.getEthosScore(demos, walletAddress)` * `Identities.getEthosIdentities(demos, address?)` * `Identities.addEthosIdentity(demos, payload)` — EVM-only with payload validation. * `Identities.removeEthosIdentity(demos, payload)` *** ## v2.11.0 *February 22, 2026* Foundational housekeeping release ahead of the identity wave (Ethos, Human Passport, L2PS messaging) that lands across v2.11.1–v2.11.5. * Initial token structure scaffolding. * Solana `contract_write` test cleanup. * Jest tsconfig fixes. *** ## v2.10.2 *February 13, 2026* * Update `addWeb2IdentityViaTLSN` method (PR #77). Generic Web2 identity assignment via TLSNotary, exposed as `addWeb2IdentityViaTLSN(context, url, proof, demos)` and `removeWeb2IdentityViaTLSN(context, demos)`. *** ## v2.10.1 *February 12, 2026* * `refactor`: change `node-forge` import from namespace to default to fix L2PS imports under stricter bundler resolution (PR #76). *** ## v2.10.0 *February 10, 2026* Identity-via-TLSNotary suite (PR #75). Adds add/remove methods for several identity providers attested via TLSNotary proofs. * `addGithubIdentityViaTLSN` / `removeGithubIdentityViaTLSN` * `addDiscordIdentityViaTLSN` / `removeDiscordIdentityViaTLSN` * `addTelegramIdentityViaTLSN` / `removeTelegramIdentityViaTLSN` * Nomis identity helpers: `getNomisScore`, `addNomisIdentity`, `removeNomisIdentity`. See [Nomis identity](/sdk/web2/identities/nomis). * TRON multichain support (PR #56). See [TRON](/sdk/cross-chain/tron). *** ## December 2024 Updates *December 2024* ### New Chain Support Several new blockchain networks have been added to the cross-chain SDK: #### NEAR Protocol Full support for NEAR blockchain including: * Token transfers with `preparePay` and `preparePays` * Account creation and deletion * Message signing and verification * See [NEAR documentation](/sdk/cross-chain/near) for details #### Cosmos Hub (ATOM) IBC-compatible Cosmos Hub support: * Native ATOM transfers via IBC SDK * Uses existing IBC infrastructure with Cosmos-specific configuration * Testnet: `rpc.provider-sentry-01.ics-testnet.polypore.xyz` * Mainnet: `cosmos-rpc.publicnode.com` #### Bitcoin (BTC) SegWit (P2WPKH) Bitcoin support: * Native BTC transfers * UTXO management and balance checking * See [BTC documentation](/sdk/cross-chain/btc) for details #### Polygon (Amoy Testnet) EVM-compatible Polygon support: * Polygon Amoy testnet for development * Polygon mainnet support * Chain ID: 137 (mainnet), 80002 (Amoy) #### XRP Ledger Send Functionality Enhanced XRPL support with full send capabilities: * Token transfers via `preparePay` * Multiple transfer support via `preparePays` * See [XRPL documentation](/sdk/cross-chain/xrpl) for details ### Identity Verification Updates #### GitHub OAuth Flow New OAuth-based identity verification for GitHub: * Seamless one-click authentication * Node-signed attestations with 5-minute validity * Complementary to existing gist-based verification * See [GitHub identity documentation](/sdk/web2/identities/github) for details #### Discord Identity Support Full Discord identity verification: * Message-based proof verification * Support for all Discord domains (standard, PTB, Canary, legacy) * See [Discord identity documentation](/sdk/web2/identities/discord) for details ### Infrastructure Improvements * Improved error handling in multichain executors * Enhanced logging with log rotation support * Node diagnostic tooling (`node-doctor`) *** ## The HTTP Rewrite *August 20th, 2024* The SDK with the HTTP rewrite of the `demos` object is published at [@kynesyslabs/demosdk-http](https://www.npmjs.com/package/@kynesyslabs/demosdk-http) on NPM. ```ts theme={null} import { demos } from "@kynesyslabs/demosdk-http/websdk" ``` ### Connecting to a node To connect to a node, use the `connect` method. ```ts theme={null} const rpc_url = "http://localhost:53550" await demos.connect(rpc_url) assert(demos.connected === true) ``` This will ping the rpc and return a `true` if the rpc is found, or throw an error. Once connected, you can now make unauthenticated requests (node calls). ```ts theme={null} const lastBlockHash = await demos.getLastBlockHash() ``` ### Connecting a wallet To make authenticated calls to the node (eg. confirming or broadcasting transactions), you need to connect a keypair to the demos object. ```ts theme={null} const identity = demos.DemosWebAuth.getInstance() await identity.create() const pubKey = await demos.connectWallet(identity.keypair.privateKey) assert(demos.walletConnected === true) ``` You can also connect a wallet using a mnemonic. ```ts theme={null} const mnemonic = "property gym walk decorate laundry grab cabin outer artist nest castle vote" const pubKey = await demos.connectWallet(mnemonic, { isSeed: true, }) ``` Or if you have a bip39 seed. ```ts theme={null} const mnemonic = "property gym walk decorate laundry grab cabin outer artist nest castle vote" const seed = bip39.mnemonicToSeedSync(mnemonic) const pubKey = await demos.connectWallet(seed, { isSeed: true, }) ``` The process of converting a mnemonic to a keypair is defined at `Cryptography.newFromSeed` (in `@/encryption/Cryptography.ts`). You can get the address of the connected wallet using the `demos.getAddress()` method. With the wallet connected, you can now send authenticated requests to the node. ```ts theme={null} const validityData = await demos.confirm(tx) ``` ### Resetting the demos object Once you're done, you can reset the demos object. ```ts theme={null} demos.disconnect() ``` Calling `demos.disconnect` won't log out the `DemosWebAuth` instance. You need to call `identity.logout()` to reset that. ## Decoupling DemosWebAuth from the websdk *August 30th, 2024* We've decoupled the `DemosWebAuth` from the helper methods and objects in the websdk. That simply means that you'll now need to pass the keypair to the methods that need it, instead of them reference the global `DemosWebAuth` instance. Here's a list of affected methods: #### 1. DemosTransactions.sign This method was previously using the global `DemosWebAuth` instance to sign transactions. Now you need to pass the keypair to the method. ```ts theme={null} // from: DemosTransactions.sign(raw_tx: Transaction) // to: DemosTransactions.sign(raw_tx: Transaction, keypair: IKeyPair) ``` You can use the keypair connected to the demos object with `DemosTransactions.sign` you can call `demos.tx.sign(tx)` instead. #### 2. prepareWeb2Payload The `prepareWeb2Payload` method now requires payload parameters and a keypair for signing the `Transaction`. ```ts theme={null} // from: prepareWeb2Payload( action = "GET", url = "https://icanhazip.com", ... ) // to: prepareWeb2Payload( params: IPrepareWeb2PayloadParams = { action: "GET", url: "https://icanhazip.com", ... }, keypair: IKeyPair, ) ``` To use the keypair connected to the demos object with `prepareWeb2Payload`, you can call `demos.web2.preparePayload(params)` instead. #### 3. prepareXMPayload The `prepareXMPayload` method now requires a `XMScript` and a keypair for signing the `Transaction`. ```ts theme={null} // from: prepareXMPayload(xm_payload: XMScript) // to: prepareXMPayload(xm_payload: XMScript, keypair: IKeyPair) ``` To use the keypair connected to the demos object with `prepareXMPayload`, you can call `demos.xm.preparePayload(xm_payload)` instead. #### 4. Wallet.transfer ```ts theme={null} // from: Wallet.transfer(to: Address, amount: number) // to: Wallet.transfer(to: Address, amount: number, keypair: IKeyPair) ``` # Base Operation Source: https://docs.kynesys.xyz/sdk/cookbook/demoswork/base-operation The base operation allow you to group work steps and operations for execution without any additional logic. # Base Operation The base operation allow you to group work steps and operations for execution without any additional logic. ```ts theme={null} import { BaseOperation, DemosWork, prepareXMStep, } from "@kynesyslabs/demosdk/demoswork" // Work steps — pass a fully-formed XMScript to prepareXMStep. // See "Creating Work Steps" for how to build sendEth / sendSol. const sendEth = prepareXMStep(ethXmScript) const sendSol = prepareXMStep(solXmScript) // Base operation const base = new BaseOperation() base.addWork(sendEth, sendSol) const work = new DemosWork() work.push(base) ``` Work steps are created and added to the base operation using the `addWork` method. You can also pass the work steps and operations to the constructor of the `BaseOperation` class. ```ts theme={null} const base = new BaseOperation(checkBTCPrice, checkEthPrice) ``` You can also use operations (eg. conditional operation) and work steps with the `BaseOperation` class. # Conditional Operation Source: https://docs.kynesys.xyz/sdk/cookbook/demoswork/conditional-operation There are two ways to go about creating a conditional operation. # Conditional Operation There are two ways to go about creating a conditional operation. 1. Using the `if`, `then` and `else` methods of the `ConditionalOperation` class. 2. Creating `Condition` objects and passing them to the constructor of the `ConditionalOperation` class. #### 1. Using the `if`, `then` and `else` methods ```ts theme={null} import { EVM } from "@kynesyslabs/demosdk/xm-websdk" import { prepareXMScript } from "@kynesyslabs/demosdk/websdk" import { DemosWork, prepareWeb2Step, prepareXMStep, ConditionalOperation, } from "@kynesyslabs/demosdk/demoswork" const address = "0x8A6575025DE23CB2DcB0fE679E588da9fE62f3B6" const isMember = prepareWeb2Step({ url: `https://api.[redacted].com/v1/eth_sepolia/address/${address}`, method: "GET", }) // Web2 step to do a POST API call const addMember = prepareWeb2Step({ url: `https://api.[redacted].com/v1/eth_sepolia/address/${address}?key=ckey_5a044cf0034a43089e6b308b023`, method: "POST", }) // XM step to send ETH const evm = await EVM.create("https://rpc.ankr.com/eth_sepolia") await evm.connectWallet(process.env.EVM_KEY) const payload = await evm.prepareTransfer(address, "0.25") const xmscript = prepareXMScript({ chain: "eth", subchain: "sepolia", type: "pay", signedPayloads: [payload], }) const releaseFunds = prepareXMStep(xmscript) // Conditional operation const operation = new ConditionalOperation() operation .if(isMember.output.statusCode, "==", 200) .then(releaseFunds) .else(addMember) // Creating a DemosWork object and indexing the operation const work = new DemosWork() work.push(operation) ``` #### Using the `Condition` class The `Condition` class can be used to construct a conditional operation without using the `if`, `then` and `else` methods. `Condition` objects can be reused and combined to create more complex conditional operations. A condition looks is implemented as follows: ```ts theme={null} import { operators } from "@kynesyslabs/demosdk/types" class Condition { value_a: any operator: operators value_b: any action: WorkStep | DemosWorkOperation } ``` The `value_a` and `value_b` fields are the operands of the condition. The `operator` field determines the type of condition. The `action` field is the work step or operation that will be executed if the condition is met. The `value_a` and `value_b` fields can accept any of the following: 1. A work step or operation output 2. Another condition 3. A static value You can create the same conditional operation as the previous example using the `Condition` class as follows: ```ts theme={null} import { EVM } from "@kynesyslabs/demosdk/xm-websdk" import { prepareXMScript } from "@kynesyslabs/demosdk/websdk" import { Condition, DemosWork, prepareWeb2Step, prepareXMStep, ConditionalOperation, } from "@kynesyslabs/demosdk/demoswork" const address = "0x8A6575025DE23CB2DcB0fE679E588da9fE62f3B6" const isMember = prepareWeb2Step({ url: `https://api.[redacted].com/v1/eth_sepolia/address/${address}`, method: "GET", }) // Web2 step to do a POST API call const addMember = prepareWeb2Step({ url: `https://api.[redacted].com/v1/eth_sepolia/address/${address}?key=ckey_5a044cf0034a43089e6b308b023`, method: "POST", }) // XM step to send ETH const evm = await EVM.create("https://rpc.ankr.com/eth_sepolia") await evm.connectWallet(process.env.EVM_KEY) const payload = await evm.prepareTransfer(address, "0.25") const xmscript = prepareXMScript({ chain: "eth", subchain: "sepolia", type: "pay", signedPayloads: [payload], }) const releaseFunds = prepareXMStep(xmscript) // Condition to check if the user is a member const checkIsMember = new Condition({ value_a: isMember.output.statusCode, operator: "==", value_b: 200, action: releaseFunds, }) // Fallback condition — the all-null shape mirrors what // ConditionalOperation.else() produces internally: a default branch // that runs when no preceding condition matches. const notMember = new Condition({ value_a: null, operator: null, value_b: null, action: addMember, }) // Creating a conditional operation const operation = new ConditionalOperation(checkIsMember, notMember) // Creating a DemosWork object and indexing the operation const work = new DemosWork() work.push(operation) ``` ### Combining conditions You can combine conditions to create more complex conditions as shown below: ```ts theme={null} // equality const booleanCondition = new Condition({ value_a: true, operator: "==", value_b: true, }) // greater than const greaterThanCondition = new Condition({ value_a: 10, operator: ">", value_b: 5, }) // and const combinedCondition = new Condition({ value_a: booleanCondition, operator: "&&", value_b: greaterThanCondition, }) // not const notCondition = new Condition({ value_a: booleanCondition, operator: "not", value_b: null, }) ``` ### Referencing web2 payloads in conditions Web2 requests return various shapes of payload when executed, including plain text or JSON. Let's illustrate how you can reference these payloads using a GET request to read the current BTC price. ```typescript theme={null} const getBTCPrice = prepareWeb2Step({ url: "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd", method: "GET", }) // {"bitcoin":{"usd":68233}} ``` The value we can use in a condition is nested inside the `bitcoin` object under the `usd` key. We can reference the value as shown below: ```typescript theme={null} new Condition({ value_a: getBTCPrice.output["bitcoin.usd"], ... }) ``` When dealing with web2 endpoints that return a single value, eg. a string or number, or when you want to reference the whole payload, you can do the following: ```typescript theme={null} new Condition({ value_a: getBTCPrice.output, ... }) ``` # Creating Work Steps Source: https://docs.kynesys.xyz/sdk/cookbook/demoswork/creating-work-steps You can create a web2 workstep to performa a `GET` request to `https://icanhazip.com` using the `prepareWeb2Step` function. # Creating work steps ### Creating a web2 workstep You can create a web2 workstep to performa a `GET` request to `https://icanhazip.com` using the `prepareWeb2Step` function. ```ts theme={null} import { prepareWeb2Step } from "@kynesyslabs/demosdk/demoswork" const getIP = prepareWeb2Step({ method: "GET", url: "https://icanhazip.com", }) ``` ### Creating a XM workstep THe process of creating a cross chain workstep to send some ETH to an address can be described as follows: 1. Generating a payload to send ETH 2. Create a `XMScript` with the payload 3. Create an XM step with the `XMScript` ```ts theme={null} import { EVM } from "@kynesyslabs/demosdk/xm-websdk" import { prepareXMStep } from "@kynesyslabs/demosdk/demoswork" import { prepareXMScript } from "@kynesyslabs/demosdk/websdk" // 1. Getting EVM payload const evm_rpc = "https://rpc.ankr.com/eth_sepolia" const evm_key = process.env.EVM_KEY const evm = await EVM.create(evm_rpc) await evm.connectWallet(evm_key) const payload = await evm.prepareTransfer( "0x8A6575025DE23CB2DcB0fE679E588da9fE62f3B6", "0.25", ) // 2. Creating a XMScript const xmscript = prepareXMScript({ chain: "eth", subchain: "sepolia", type: "pay", signedPayloads: [payload], }) // 3. Creating a XM step const sendEth = prepareXMStep(xmscript) ``` # Demoswork Source: https://docs.kynesys.xyz/sdk/cookbook/demoswork/overview Learn how to create and manage work steps in the Demos network # Demoswork ### Table of contents Learn how to create work steps Implement conditional logic in work steps Understand base operations Sign and broadcast work steps # Signing And Broadcasting Source: https://docs.kynesys.xyz/sdk/cookbook/demoswork/signing-and-broadcasting To convert a Demoswork object to a transaction, use the `prepareDemosWorkPayload` function. # Signing and broadcasting To convert a Demoswork object to a transaction, use the `prepareDemosWorkPayload` function. ```ts theme={null} import { Demos } from "@kynesyslabs/demosdk/websdk" import { prepareDemosWorkPayload } from "@kynesyslabs/demosdk/demoswork" const demos = new Demos() await demos.connect("https://node2.demos.sh") await demos.connectWallet(mnemonic) // `work` is the DemosWork object built in the previous steps // (see "Creating Work Steps", "Conditional Operation", or "Base Operation"). const tx = await prepareDemosWorkPayload(work, demos) ``` ### Broadcasting the transaction You can broadcast the transaction using the `confirm` and `broadcast` methods of the `demos` object. ```ts theme={null} // Confirming the transaction const validityData = await demos.confirm(tx) // Broadcasting the transaction const res = await demos.broadcast(validityData) console.log("res:", res) ``` If you need a deterministic confirmation that the work transaction landed on chain, use [`broadcastAndWait`](/sdk/websdk/transactions/broadcasting-a-transaction#broadcast-and-wait-for-inclusion) instead — it broadcasts and then polls until the transaction reaches a terminal `included` or `failed` state. ```ts theme={null} const { broadcast, status } = await demos.broadcastAndWait(validityData) console.log("status:", status.state, "block:", status.blockNumber) ``` # Evm Contract Write Transactions Source: https://docs.kynesys.xyz/sdk/cookbook/evm-contract-write-transactions The `contract_write` operation enables interaction with smart contracts on EVM-compatible blockchains through the Demos Network. This functionality al... # EVM Contract Write Transactions The `contract_write` operation enables interaction with smart contracts on EVM-compatible blockchains through the Demos Network. This functionality allows you to prepare, sign, and execute contract method calls with custom parameters and options. ### Workflow ```typescript theme={null} // 1. Get contract instance const contract = await instance.getContractInstance(contractAddress, abi) // 2. Write to contract (generates signed transaction) const signedTx = await instance.writeToContract(contract, methodName, args, options) // 3. Prepare XM script for Demos Network const xmscript = prepareXMScript({ chain: "eth", signedPayloads: [signedTx], subchain: "sepolia", type: "contract_write", is_evm: true, }) // 4. Prepare and broadcast through Demos Network const signedDemosTx = await prepareXMPayload(xmscript, demos) const validityData = await demos.confirm(signedDemosTx) const result = await demos.broadcast(validityData) ``` ### API Reference #### `writeToContract(contract, functionName, args, options?)` Prepares and signs a smart contract transaction for execution. **Parameters:** * `contract`: Contract instance (obtained via `getContractInstance`) * `functionName`: Name of the contract method to call * `args`: Array of arguments for the method * `options` (optional): Transaction options object **Options Object:** ```typescript theme={null} { gasLimit?: number; // Custom gas limit for the transaction value?: string; // ETH value to send (in ETH units, for payable functions) } ``` **Returns:** `Promise` - Signed transaction hex string #### `prepareXMScript(params)` Creates an XM script for contract write operations. **Parameters:** ```typescript theme={null} { chain: string; // Target blockchain (e.g., "eth") subchain: string; // Network (e.g., "sepolia", "mainnet") signedPayloads: string[]; // Array of signed transactions type: "contract_write"; // Operation type is_evm: boolean; // Whether target chain is EVM-compatible } ``` ### Examples #### Basic ERC20 Token Transfer ```typescript theme={null} import { parseEther } from "ethers" import { EVM } from "@kynesyslabs/demosdk/xm-websdk" import { prepareXMScript, prepareXMPayload, Demos } from "@kynesyslabs/demosdk/websdk" // Initialize EVM instance and Demos const instance = new EVM(chainProviders.eth.sepolia) const demos = new Demos() // Connect wallet and Demos await instance.connect() await instance.connectWallet(evmPrivateKey) await demos.connect(rpc) await demos.connectWallet(mnemonic) // ERC20 ABI for transfer function const erc20ABI = [ { constant: false, inputs: [ { name: "dst", type: "address" }, { name: "wad", type: "uint256" }, ], name: "transfer", outputs: [{ name: "", type: "bool" }], payable: false, stateMutability: "nonpayable", type: "function", }, ] // Get contract instance const tokenAddress = "0x1234567890123456789012345678901234567890" const contract = await instance.getContractInstance(tokenAddress, JSON.stringify(erc20ABI)) // Prepare transfer const recipient = "0xa2f64eec3E69C0B2E9978AB371A16eaA3a1Cf793" const amount = parseEther("1.0") // Sign transaction const signedTx = await instance.writeToContract(contract, "transfer", [recipient, amount]) // Prepare XM script const xmscript = prepareXMScript({ chain: "eth", signedPayloads: [signedTx], subchain: "sepolia", type: "contract_write", is_evm: true, }) // Execute through Demos Network const signedDemosTx = await prepareXMPayload(xmscript, demos) const validityData = await demos.confirm(signedDemosTx) const result = await demos.broadcast(validityData) ``` #### Payable Contract with Custom Options ```typescript theme={null} // Payable contract ABI (e.g., deposit function) const payableABI = [ { constant: false, inputs: [], name: "deposit", outputs: [], payable: true, stateMutability: "payable", type: "function", }, ] const contractAddress = "0x1234567890123456789012345678901234567890" const contract = await instance.getContractInstance(contractAddress, JSON.stringify(payableABI)) // Call payable function with ETH value and custom gas const signedTx = await instance.writeToContract( contract, "deposit", [], // No arguments for this function { gasLimit: 100000, // Custom gas limit value: "1" // Send 1 ETH with the transaction } ) // Create XM script and execute const xmscript = prepareXMScript({ chain: "eth", signedPayloads: [signedTx], subchain: "sepolia", type: "contract_write", is_evm: true, }) const signedDemosTx = await prepareXMPayload(xmscript, demos) const validityData = await demos.confirm(signedDemosTx) const result = await demos.broadcast(validityData) ``` # Code Examples Source: https://docs.kynesys.xyz/sdk/cookbook/overview Practical examples and recipes for common SDK use cases including storage programs, transactions, and swaps. # Code Examples # Examples Source: https://docs.kynesys.xyz/sdk/cookbook/storage-programs/examples Concrete, end-to-end recipes for Storage Programs against the v4.0.3 SDK. # Storage Programs Examples Each recipe below uses the real v4.0.3 SDK surface: * **`StorageProgram.*` static helpers** to build typed payloads. * **`demos.storagePrograms.sign(payload)`** to produce a signed transaction. * **`demos.confirm` + `demos.broadcast`** (or `broadcastAndWait`) to submit it. * **`demos.storagePrograms.read(address)`** to query state via RPC (free, no transaction). For deeper background on operations, ACL modes, and resource limits, see the [Operations](/sdk/storage-programs/operations) and [Access Control](/sdk/storage-programs/access-control) guides. ## Boilerplate Every example below assumes this setup: ```typescript theme={null} import { Demos } from "@kynesyslabs/demosdk/websdk" import { StorageProgram } from "@kynesyslabs/demosdk/storage" const demos = new Demos() await demos.connect("https://node2.demos.sh") await demos.connectWallet(mnemonic) const ownerAddress = demos.getAddress() ``` A small helper to broadcast a signed storage-program transaction with deterministic confirmation: ```typescript theme={null} async function submit(payload: ReturnType) { const tx = await demos.storagePrograms.sign(payload) const validityData = await demos.confirm(tx) const { broadcast, status } = await demos.broadcastAndWait(validityData) if (status.state !== "included") { throw new Error(`Storage tx failed at block ${status.blockNumber}`) } return { tx, broadcast, status } } ``` `StorageProgram.createStorageProgram` takes a **nonce** (the sender's current account nonce) so that the derived storage address is unique per transaction from the same deployer. Fetch the nonce from `demos.getAddressNonce(ownerAddress)` before each create. ## Example 1 — Public user profile A read-only-for-the-world profile: anyone can fetch it, only the deployer can write. ```typescript theme={null} async function createPublicProfile(displayName: string, bio: string) { const nonce = await demos.getAddressNonce(ownerAddress) const payload = StorageProgram.createStorageProgram( ownerAddress, "userProfile", { displayName, bio, joinedAt: Date.now(), }, "json", { mode: "public" }, // public read, owner-only write { nonce, salt: "v1" }, ) await submit(payload) return payload.storageAddress // stor-… } async function updateProfileBio(storageAddress: string, bio: string) { const payload = StorageProgram.writeStorage( storageAddress, { bio, lastUpdated: Date.now() }, "json", ) await submit(payload) } async function readProfile(storageAddress: string) { return await demos.storagePrograms.read(storageAddress) } ``` `writeStorage` replaces the entire `data` field — it does not merge. Keys you don't pass are dropped. Use the granular helpers (`setField`, `appendItem`, etc.) for partial updates. ## Example 2 — Restricted team workspace Two ACL groups: editors (read + write) and viewers (read-only). Other addresses cannot read or write. ```typescript theme={null} async function createTeamWorkspace( teamName: string, editors: string[], viewers: string[], ) { const nonce = await demos.getAddressNonce(ownerAddress) const payload = StorageProgram.createStorageProgram( ownerAddress, `team-${teamName}`, { teamName, createdAt: Date.now(), announcements: [], }, "json", { mode: "restricted", groups: { editors: { members: editors, permissions: ["read", "write"], }, viewers: { members: viewers, permissions: ["read"], }, }, }, { nonce }, ) await submit(payload) return payload.storageAddress } ``` To rotate membership later, push a new ACL via `updateAccessControl`: ```typescript theme={null} async function rotateEditors(storageAddress: string, editors: string[]) { const payload = StorageProgram.updateAccessControl(storageAddress, { mode: "restricted", groups: { editors: { members: editors, permissions: ["read", "write"], }, }, }) await submit(payload) } ``` Only the program's owner can call `updateAccessControl`. ## Example 3 — Binary attachment with metadata For non-JSON payloads (PDFs, images, archives), encode the bytes as base64 and use `encoding: "binary"`. Storage Programs are capped at **1 MB** (`MAX_SIZE_BYTES = 1,048,576` bytes) per program — splitting larger blobs across multiple programs is the standard pattern. ```typescript theme={null} async function uploadDocument( name: string, base64Body: string, mimeType: string, ) { const nonce = await demos.getAddressNonce(ownerAddress) const payload = StorageProgram.createStorageProgram( ownerAddress, `doc-${name}`, base64Body, "binary", { mode: "owner" }, // private to the deployer { nonce, metadata: { filename: name, mimeType, size: base64Body.length, }, }, ) await submit(payload) return payload.storageAddress } async function deleteDocument(storageAddress: string) { const payload = StorageProgram.deleteStorageProgram(storageAddress) await submit(payload) } ``` `metadata` is opaque to consensus but is returned alongside the data on read, so it's a good place to stash filename, MIME type, encoded size, or content hash. ## Where to go next * [Operations guide](/sdk/storage-programs/operations) — when each operation is appropriate. * [Access Control guide](/sdk/storage-programs/access-control) — full ACL matrix and security checklist. * [RPC Queries](/sdk/storage-programs/rpc-queries) — fast read paths for clients. * [API Reference](/sdk/storage-programs/api-reference) — full method signatures and payload shapes. # Storage Programs Source: https://docs.kynesys.xyz/sdk/cookbook/storage-programs/overview Practical patterns that show how to combine the Storage Program SDK primitives in real-world applications. # Storage Program Recipes Practical patterns that show how to combine the Storage Program SDK primitives in real-world applications. * [Examples](/sdk/cookbook/storage-programs/examples) – end-to-end scenarios for user profiles, games, collaboration, e-commerce, and more. Looking for the core concepts instead? Head back to the [Storage Programs overview](/sdk/storage-programs/overview) in the SDK section. # Crosschain Swap Source: https://docs.kynesys.xyz/sdk/cookbook/swap/crosschain-swap Swap tokens between chains using the SDK's RubicBridge integration. # Crosschain SWAP `RubicBridge` is a thin SDK client for the node-side Rubic bridge integration. It exposes two methods — `getTrade` for quoting a swap, and `executeTrade` for committing it — both of which proxy through the connected DEMOS RPC. ## How to SWAP from one chain to another ### 1. Connect to the network ```ts theme={null} import { Demos, RubicBridge } from "@kynesyslabs/demosdk/websdk" import { BridgeTradePayload, SupportedChains, } from "@kynesyslabs/demosdk/bridge" const rpc = "https://node2.demos.sh" const demos = new Demos() await demos.connect(rpc) await demos.connectWallet(mnemonic) ``` ### 2. Create a `RubicBridge` instance `RubicBridge` is stateless — there are no constructor arguments. The chain and payload are passed per call. ```ts theme={null} const bridge = new RubicBridge() ``` ### 3. Build the trade payload `BridgeTradePayload.amount` and the chain IDs refer to the **source chain's** native units, **not** Demos OS. For example, an EVM swap of 10 USDT uses `amount: 10` because Rubic itself handles the on-chain decimal conversion. ```ts theme={null} // Swap 10 USDT from Polygon (chain 137) to Ethereum (chain 1). const payload: BridgeTradePayload = { fromToken: "USDT", toToken: "USDT", amount: 10, fromChainId: 137, toChainId: 1, } ``` Valid `fromToken` / `toToken` values are `"NATIVE"`, `"USDC"`, and `"USDT"`. `"NATIVE"` refers to the source chain's native token (e.g. ETH on Ethereum, MATIC on Polygon) — **not** Demos `DEM`. `SupportedChains` is exported as a string-valued object with these keys: ```ts theme={null} export const SupportedChains = { ETH: "ETH", POLYGON: "POLYGON", BSC: "BSC", AVALANCHE: "AVALANCHE", OPTIMISM: "OPTIMISM", ARBITRUM: "ARBITRUM", LINEA: "LINEA", BASE: "BASE", SOLANA: "SOLANA", } ``` The first argument to `getTrade` / `executeTrade` is the **source chain key** (`SupportedChains.POLYGON` in this example). ### 4. Quote the trade ```ts theme={null} const trade = await bridge.getTrade( demos, SupportedChains.POLYGON, payload, ) ``` `getTrade` returns the standard `RPCResponse`. Inspect `trade.response` for the quoted route and pricing before committing. ### 5. Execute the trade If the quote looks good, commit the swap with `executeTrade`. The same `payload` is passed — there is no separate "trade object" to forward. ```ts theme={null} const receipt = await bridge.executeTrade( demos, SupportedChains.POLYGON, payload, ) ``` The result wraps the source-chain transaction hash, e.g. ```ts theme={null} "0x2342692074a7484f3ac9713d36cd23fdc1c51810d03639e5cf651bfefb62fdc3" ``` [PolygonScan](https://polygonscan.com/tx/0x2342692074a7484f3ac9713d36cd23fdc1c51810d03639e5cf651bfefb62fdc3) For local development against an unfunded source chain, `RubicBridge.executeMockTrade(demos, chain, wrappedTrade)` accepts a `WrappedCrossChainTrade` (from `rubic-sdk`) and exercises the same RPC path without broadcasting on the foreign chain. # Swap Source: https://docs.kynesys.xyz/sdk/cookbook/swap/overview Learn how to perform cross-chain token swaps using the Demos SDK. # SWAP # Crosschain Transaction Source: https://docs.kynesys.xyz/sdk/cookbook/transactions/crosschain-transaction You can perform a crosschain transaction via the DEMOS network with the help of the DEMOS sdk. # Crosschain Transaction You can perform a crosschain transaction via the DEMOS network with the help of the DEMOS sdk. Here is how you can send tokens on Sepolia testnet via DEMOS using the sdk: ### 0. Imports The following imports will be needed to create the transaction. ```typescript theme={null} import { Demos, prepareXMPayload, prepareXMScript, } from "@kynesyslabs/demosdk/websdk"; import { EVM } from "@kynesyslabs/demosdk/xm-websdk"; ``` ### 1. Creating the EVM payload You can create the payload for Sepolia via the crosschain module of the sdk as follows: ```typescript theme={null} const sepolia_rpc = "https://rpc.ankr.com/eth_sepolia" const evm = await EVM.create(sepolia_rpc); await evm.connectWallet( "e0a00e307....." // fill in with your sepolia private key ); const evm_tx = await evm.preparePay( "0xda3ea78Af43E6B1c63A08cD0058973F14e5556b0", "0.000000001", ) ``` The `evm_tx` will be a signed transaction to move 0.000000001 Sepolia ETH from the connected wallet to`0xda3ea78Af43E6B1c63A08cD0058973F14e5556b0`. ### 2. Converting the payload into an XMScript The [XMScript](../../cross-chain/the-xmscript) contains information that helps a DEMOS node understand what to do with a signed crosschain payload. ```typescript theme={null} const xmscript = prepareXMScript({ chain: "eth", subchain: "sepolia", signedPayloads: [evm_tx], type: "pay", }); ``` The `xmscript` will be embedded into a DEMOS transaction and sent to the DEMOS network in the upcoming sections. ### 3. Creating a DEMOS instance A DEMOS identity is needed to sign the outgoing DEMOS transaction. ```typescript theme={null} const demos = new Demos() const demos_rpc = "https://node2.demos.sh"; await demos.connect(demos_rpc); const mnemonic = demos.newMnemonic() await demos.connectWallet(mnemonic) ``` ### 4. Converting the XMScript to a Demos transaction You can convert the `xmscript` into a DEMOS transaction by calling `prepareXMPayload` and passing the `xmscript` and the `Demos` instance. ```typescript theme={null} const tx = await prepareXMPayload(xmscript, demos); ``` The `tx` will be a signed Demos transaction containing the Sepolia tx. ### 5. Broadcasting to a Demos node You can now broadcast the transaction as shown: ```typescript theme={null} // confirm tx const validityData = await demos.confirm(tx); console.log("validityData", validityData); // execute const res = await demos.broadcast(validityData); console.log("res", res); ``` # Native Transactions Source: https://docs.kynesys.xyz/sdk/cookbook/transactions/native-transactions You can send DEM using the Demos sdk as shown below: # Native Transactions You can send DEM using the Demos sdk as shown below: ### 1. Connect to the network ```javascript theme={null} import { Demos } from "@kynesyslabs/demosdk/websdk" // Connect to the network const rpc = "https://node2.demos.sh" const demos = new Demos() await demos.connect(rpc) // Create and connect your wallet const mnemonic = demos.newMnemonic() await demos.connectWallet(mnemonic) ``` ### 2. Create a transaction You can now create a transaction by passing the address and the amount to `demos.transfer`. The amount can be either a `bigint` in **OS** (preferred, full precision; `1 DEM = 10^9 OS`) or a legacy `number` in whole **DEM** (deprecated, removed in v4). ```typescript theme={null} import { denomination } from "@kynesyslabs/demosdk" // 2a. Preferred — bigint OS via the denomination helper const tx = await demos.transfer( "0x6690580a02d2da2fefa86e414e92a1146ad5357fd71d594cc561776576857ac5", denomination.demToOs(100), // 100 DEM → 100_000_000_000n OS ) // 2b. Equivalent — raw bigint OS literal const tx2 = await demos.transfer( "0x...", 100_000_000_000n, ) // 2c. Legacy — number in whole DEM (deprecated) const tx3 = await demos.transfer("0x...", 100) ``` The transaction returned by `demos.transfer` is signed using the wallet connected to the `demos` object. `demos.transfer` is an alias to `demos.pay`. See [Amounts & Denominations](/sdk/websdk/transactions/denominations) for the full denomination reference, including `getNetworkInfo` fork detection and the `SubDemPrecisionError` guard. ### 3. Broadcasting the transaction ```typescript theme={null} // Confirm the transaction const validityData = await demos.confirm(tx) console.log("Validity data", validityData) // Broadcast the transaction const broadcastRes = await demos.broadcast(validityData) console.log("Broadcast result", broadcastRes) ``` # Transactions Source: https://docs.kynesys.xyz/sdk/cookbook/transactions/overview Learn about transaction types and patterns in the Demos network # Transactions ### Table of contents Learn how to perform cross-chain transactions Understanding native transactions # Aptos Source: https://docs.kynesys.xyz/sdk/cross-chain/aptos The Aptos blockchain uses the Move programming language and provides a powerful environment for smart contracts and decentralized applications. The De... # APTOS ## Aptos Cross-Chain SDK The Aptos blockchain uses the Move programming language and provides a powerful environment for smart contracts and decentralized applications. The Demos Network SDK provides seamless integration with Aptos through our XM (Cross-chain Messaging) architecture. ### Key Concepts **Demos Network Architecture**: Payment and contract operations go through Demos, ensuring network participation and cross-chain compatibility. This relay-first approach provides enhanced security and enables true cross-chain functionality. **Move Language**: Aptos uses Move, a resource-oriented programming language where smart contracts are organized as modules deployed at specific addresses. **Type Arguments**: Many Move functions require explicit type arguments, especially when working with generic types like coins. ### Installation ```bash theme={null} bun add @kynesyslabs/demosdk@latest @aptos-labs/ts-sdk ``` ### Quick Start ```javascript theme={null} import { Network } from '@aptos-labs/ts-sdk' import { APTOS } from '@kynesyslabs/demosdk/xmcore' // Initialize with network const aptos = new APTOS("", Network.DEVNET) // Using a custom RPC endpoint // new APTOS("https://custom-aptos-rpc.com", Network.DEVNET) await aptos.connect() // Connect wallet const privateKey = "0x..." // Your private key await aptos.connectWallet(privateKey) ``` ### Balance Queries You can get the coin balance as shown below: ```javascript theme={null} // Get APT balance const balance = await aptos.getBalance(address) console.log("APT balance", balance) // Get specific coin balance const coinBalance = await aptos.getCoinBalance("0x1::aptos_coin::AptosCoin", address) console.log("AptosCoin balance", coinBalance) ``` ### Token Transfer You can make payments on Aptos via Demos as shown below. Please check out the [authentication](../websdk/authentication/) docs on how to interact with the Demos network using the `Demos` object. ```typescript theme={null} import { XMScript } from "@kynesyslabs/demosdk/types" import { prepareXMPayload } from "@kynesyslabs/demosdk/websdk" const recipient = "0x..." // Amount in octa (1 APT = 100,000,000 Octas) const amount = "3000" // Get the signed tx as hex string const tx = aptos.preparePay(recipient, amount) // Broadcast the tx const payload: XMScript = { operations: { aptos_pay: { chain: "aptos", subchain: "devnet", is_evm: false, rpc: null, task: { signedPayloads: [tx], params: null, type: "pay", } } }, operations_order: ["aptos_pay"], } // Create the Demos tx const tx = await prepareXMPayload(payload, demos) console.log("tx", tx) // Validate Demos tx const validityData = await demos.confirm(tx) console.log("validityData", validityData) // Broadcast const res = await demos.broadcast(validityData) console.log("res", JSON.stringify(res, null, 2)) ``` When working with Aptos browser extensions use their documentation to create and sign the transaction. Here is some documentation: * Petra: [Connect to Petra wallet](https://petra.app/docs/connect-to-petra) and [sign a transaction](https://petra.app/docs/sending-a-transaction#sign-only) * Martian: [Detecting the provider](https://docs.martianwallet.xyz/docs/integration/detecting-the-provider) and [sign a transaction](https://docs.martianwallet.xyz/docs/methods/aptos/sign-transaction) Once you have the signed transaction as a `Uint8Array`, convert it into hex and use that to create the `XMScript` from the previous code sample: For Martian wallet, that would look like this: ```typescript theme={null} import { hexToUint8Array } from "@kynesyslabs/demosdk/encryption" const txBuffer = await window.martian.signTransaction(transaction) const txHex = uint8ArrayToHex(txBuffer) const payload: XMScript = { operations: { aptos_pay: { chain: "aptos", subchain: "devnet", is_evm: false, rpc: null, task: { signedPayloads: [txHex], params: [], type: "pay", } } }, operations_order: ["aptos_pay"], } // Proceed to create Demos tx, validate and broadcast ``` ### Smart Contract Interactions #### Reading from Contracts You can read from a contract on Aptos via Demos as shown: ```javascript theme={null} // Read the "name" property of a contract const payload = await instance.readFromContract( "0x1", "coin", "name", [], ["0x1::aptos_coin::AptosCoin"] ) // Create Demos tx const tx = await prepareXMPayload(payload, demos) console.log("tx", tx) // Validate Demos tx const validityData = await demos.confirm(tx) console.log("validityData", validityData) // Broadcast const res = await demos.broadcast(validityData) console.log("res", JSON.stringify(res, null, 2)) ``` #### Writing to Contracts You can write to a contract on Aptos via Demos as shown: ```javascript theme={null} // Write to the "transfer" method on a contract const payload = await instance.writeToContract( "0x1", // module address "coin", // module name "transfer", // function name [testAddress, "1000"], // args: [recipient, amount] ["0x1::aptos_coin::AptosCoin"] // typeArguments: coin type ) console.log("xmscript", xmscript) // Create Demos tx const tx = await prepareXMPayload(payload, demos) console.log("tx", tx) // Validate Demos tx const validityData = await demos.confirm(tx) console.log("validityData", validityData) // Broadcast const res = await demos.broadcast(validityData) console.log("res", JSON.stringify(res, null, 2)) ``` ### Signing and Verifying Messages You can sign and verify messages using the connected Aptos wallet as shown: ```typescript theme={null} // Sign message const signature = await aptos.signMessage("Hello Demos Network") // Verify message signature const publicKey = aptos.getPublicKey() const isVerified = await aptos.verifyMessage(message, signature, publicKey) ``` When working with browser wallet extensions, use their documentation to sign the message: * Martian: [https://docs.martianwallet.xyz/docs/methods/sign-message](https://docs.martianwallet.xyz/docs/methods/sign-message) * Petra: [https://petra.app/docs/signing-a-message](https://petra.app/docs/signing-a-message) The `signMessage` method provided by the injected provider returns an object with a `fullMessage` property which you can use to verify the message as follows: For Martian wallet, that looks like this: ```typescript theme={null} const { fullMessage, signature } = await window.martian.signMessage({ message, nonce, }) // Verify using the Demos Aptos SDK const isVerified = aptos.verifyMessage(fullMessage, signature, publicKey) ``` ### Utility Methods ```javascript theme={null} // Get wallet address const address = aptos.getAddress() // Validate Aptos address const isValid = aptos.isAddress("0x1") // Wait for transaction confirmation const txResponse = await aptos.waitForTransaction(txHash) ``` # Btc Source: https://docs.kynesys.xyz/sdk/cross-chain/btc The Bitcoin SDK provides an interface to interact with Bitcoin blockchains using SegWit (P2WPKH) transactions. It supports wallet connectivity, transa... # BTC The Bitcoin SDK provides an interface to interact with Bitcoin blockchains using SegWit (P2WPKH) transactions. It supports wallet connectivity, transaction preparation, and balance checking, using the bitcoinjs-lib library for core functionality. ## Wallet setup To generate new Bitcoin WIF (Wallet Import Format) private keys for **P2WPKH** wallets, you can use the following tools: * [Bitcoin Tools](https://bitcoin-tools-blond.vercel.app/) (testnet) * [Bitaddress.org](https://bitaddress.org/) ## Get tokens on the testnet For testing process, you can use following links to get token on the testnet * [Coinfaucet](https://coinfaucet.eu/en/btc-testnet/) * [Testnet Faucet](https://testnet-faucet.com/) ## Initialization Import the SDK and create a new instance: ```typescript theme={null} import { BTC } from "@kynesyslabs/demosdk/xm-websdk" const rpc_url = "https://blockstream.info/testnet/api"; const network = BTC.networks.testnet; const instance = await BTC.create(rpc_url, network); ``` ## Wallet connect Connect your wallet using a private key in WIF format: ```typescript theme={null} const privateKeyWIF = "cTb..."; // Your WIF private key await instance.connectWallet(privateKeyWIF); ``` You can view the connected wallet’s address using the `getAddress` method: ```typescript theme={null} const address = instance.getAddress(); console.log(`Address: ${address}`); ``` ## Create payload for the token transfer Prepare a signed transaction to transfer BTC using `preparePay` method: ```typescript theme={null} const receiverAddress = "tb1q..."; const amount = "560"; // Amount in satoshis const signedTx = await instance.preparePay(receiverAddress, amount); ``` The `signedTx` is a hex-encoded signed transaction ready for use in a DEMOS transaction. ## Create payload for the multiple transfers You can create multiple Bitcoin transfer payloads by using the `preparePays` method as shown: ```typescript theme={null} const transfers = [ { address: "tb1q...", amount: "550" }, ]; const signedTxs = await instance.preparePays(transfers); ``` The `signedTxs` is an array of hex-encoded signed transaction payloads. ## Checking Balance You can check the balance of the connected wallet, including change addresses as shown: ```typescript theme={null} const balance = await instance.getBalance(); console.log(`Balance: ${balance} satoshis`); ``` ## Resources * [Bitcoin Website](https://bitcoin.org/) * [BitcoinJS Documentation](https://github.com/bitcoinjs/bitcoinjs-lib) * [Blockstream Explorer](https://blockstream.info/) * [Testnet Explorer](https://blockstream.info/testnet) # Evm Source: https://docs.kynesys.xyz/sdk/cross-chain/evm The Ethereum Virtual Machine (EVM) is the runtime environment for smart contracts in Ethereum. It's a fundamental part of the Ethereum network and is ... # EVM The Ethereum Virtual Machine (EVM) is the runtime environment for smart contracts in Ethereum. It's a fundamental part of the Ethereum network and is used by many other blockchain platforms that aim to provide Ethereum-like functionality. Our EVM SDK provides a simple interface to interact with EVM-compatible blockchains. ### Supported Networks The EVM SDK supports multiple EVM-compatible networks: | Network | Mainnet | Testnet | Chain ID | | ------------- | --------------------------------------- | ---------------------------------------------------------- | -------------- | | **Ethereum** | `https://rpc.ankr.com/eth` | Sepolia: `https://ethereum-sepolia-rpc.publicnode.com` | 1 / 11155111 | | **Polygon** | `https://polygon-rpc.com` | Amoy: `https://rpc-amoy.polygon.technology` | 137 / 80002 | | **Arbitrum** | `https://arbitrum.llamarpc.com` | Sepolia: `https://sepolia-rollup.arbitrum.io/rpc` | 42161 / 421614 | | **Base** | `https://base.llamarpc.com` | Sepolia: `https://sepolia.base.org` | 8453 / 84532 | | **Avalanche** | `https://api.avax.network/ext/bc/C/rpc` | Fuji: `https://avalanche-fuji-c-chain.publicnode.com` | 43114 / 43113 | | **BSC** | `https://bsc-dataseed1.defibit.io` | Testnet: `https://data-seed-prebsc-2-s2.bnbchain.org:8545` | 56 / 97 | | **Linea** | `https://rpc.linea.build` | Goerli: `https://rpc.goerli.linea.build` | 59144 / 59140 | ### Core Concepts 1. [Accounts](https://ethereum.org/en/developers/docs/accounts/): Ethereum addresses that can hold balance and send transactions 2. [Transactions](https://ethereum.org/en/developers/docs/transactions/): Signed data packages that store a message to be sent from an externally owned account 3. [Gas](https://ethereum.org/en/developers/docs/gas/): The fee required to execute operations on the Ethereum network 4. [Smart Contracts](https://ethereum.org/en/developers/docs/smart-contracts/): Programs that run on the Ethereum blockchain ### Setting up your wallet To interact with EVM-compatible chains, you'll need a wallet with a private key. You can use tools like MetaMask or hardware wallets to generate and manage your keys securely. For testing purposes, you can use faucets to get testnet tokens: **Ethereum Sepolia:** * [Alchemy Sepolia Faucet](https://sepoliafaucet.com/) * [Chainlink Sepolia Faucet](https://faucets.chain.link/sepolia) **Polygon Amoy:** * [Polygon Faucet](https://faucet.polygon.technology/) **Arbitrum Sepolia:** * [Arbitrum Faucet](https://faucet.arbitrum.io/) **Base Sepolia:** * [Base Faucet](https://www.alchemy.com/faucets/base-sepolia) ### Initialization Import the SDK and create a new instance: ```ts theme={null} import { EVM } from "@kynesyslabs/demosdk/xm-websdk" const rpc_url = "https://sepolia.infura.io/v3/YOUR-PROJECT-ID" const instance = await EVM.create(rpc_url) ``` ### Connecting your wallet To create signed transaction payloads, you need to connect your wallet to the SDK: ```ts theme={null} const privateKey = "0x1234567890abcdef..." // Your private key await instance.connectWallet(privateKey) ``` You can view the address of your connected wallet using the `getAddress` method: ```ts theme={null} const address = instance.getAddress() console.log(`Address: ${address}`) ``` ### Creating a token transfer payload To create a signed transaction payload to transfer ETH, you can use the `preparePay` method: ```ts theme={null} const recipientAddress = "0x742d35Cc6634C0532925a3b844Bc454e4438f44e" const amount = "0.1" // Amount in ETH const signedTx = await instance.preparePay(recipientAddress, amount) ``` The `signedTx` is a signed transaction that can be used a DEMOS transaction. ### Creating multiple transfer payload You can prepare multiple signed transaction payloads at once using the `preparePays` method: ```ts theme={null} const transfers = [ { address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", amount: "0.1" }, { address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", amount: "0.2" }, ] const signedTxs = await instance.preparePays(transfers) ``` The `signedTxs` is an array of signed transaction payloads that can be used in a DEMOS transaction. ### Checking balance You can check the balance of an address using the `getBalance` method: ```ts theme={null} const balance = await instance.getBalance( "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", ) console.log(`Balance: ${balance} ETH`) ``` ### Signing messages ```javascript theme={null} const message = "Hello, world!" // signing const signature = await instance.signMessage(message) // verifying signature const verified = await instance.verifyMessage( message, signature, instance.getAddress(), ) expect(verified).toBe(true) ``` ### Working with smart contracts The SDK provides methods to interact with smart contracts. Note that `abi` must be passed as a **JSON string**, not a JavaScript array — pass it through `JSON.stringify(abi)` if you have it as an array. ```ts theme={null} const contractAddress = "0x..." const abi = JSON.stringify([/* contract ABI */]) const contract = await instance.getContractInstance(contractAddress, abi) // Read from contract const result = await instance.readFromContract(contract, "functionName", [arg1, arg2]) // Write to contract — returns the signed-and-broadcast tx hash as a string const txHash = await instance.writeToContract( contract, "functionName", [arg1, arg2], { gasLimit: 200_000, value: "0.01" }, // optional; value is in ETH (parsed via parseEther) ) ``` `readFromContract(contract, fnName, args)` calls a view/pure function and returns the raw return value. `writeToContract(contract, fnName, args, options?)` builds, signs, and broadcasts a state-changing transaction. The optional fourth argument accepts `{ gasLimit?: number; value?: string }` — `value` is the ETH amount as a decimal string and is parsed via `parseEther`. ### Listening for events You can listen for specific events or all events from a contract: ```ts theme={null} // Listen for a specific event (contract, abi, event, timeout?) const eventData = await instance.listenForEvent(contractAddress, abi, "EventName", 5000) // Listen for all events (returns a function to remove the listener) const unsubscribe = instance.listenForAllEvents(contractAddress, abi, (...args) => { console.log("event", args) }) ``` ### Cleaning up To disconnect your wallet and clean up resources, you can use the `disconnect` method: ```ts theme={null} await instance.disconnect() ``` ### Advanced usage For more advanced use cases, you can access underlying properties and methods of the EVM instance: | Property/Method | Description | API Reference | | ----------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | | `instance.provider` | The JsonRpcProvider instance for interacting with the Ethereum network | [JsonRpcProvider](https://docs.ethers.org/v6/api/providers/jsonrpc/#JsonRpcProvider) | | `instance.wallet` | The Wallet instance representing the connected account | [Wallet](https://docs.ethers.org/v6/api/wallet/#Wallet) | | `instance.contracts` | A Map of Contract instances for interacting with smart contracts | [Contract](https://docs.ethers.org/v6/api/contract/#Contract) | | `instance.signTransaction(s)` | Method(s) to sign one or multiple transactions | - | You can use these properties and methods to perform custom operations: ```ts theme={null} const blockNumber = await instance.provider.getBlockNumber() ``` ### API Reference [https://kynesyslabs.github.io/demosdk-api-ref/classes/xmwebsdk.EVM.html](https://kynesyslabs.github.io/demosdk-api-ref/classes/xmwebsdk.EVM.html) ### Resources * [Ethereum Website](https://ethereum.org) * [Ethers.js Documentation](https://docs.ethers.org/v6/) * [MetaMask](https://metamask.io/) * [Ethereum Block Explorer (Mainnet)](https://etherscan.io/) * [Sepolia Block Explorer](https://sepolia.etherscan.io/) # General Layout Of The Xm Sdks Source: https://docs.kynesys.xyz/sdk/cross-chain/general-layout-of-the-xm-sdks All the crosschain SDKs share the same interface, with a few blockchain-specific methods occuring on some SDKS. # General layout of the XM SDKs All the crosschain SDKs share the same interface, with a few blockchain-specific methods occuring on some SDKS. To demonstrate the general layout of the SDKs, let's use the EVM crosschain SDK for the web. ### Creating an SDK instance Start by instantiating the SDK class using a RPC url. ```typescript theme={null} import { EVM } from "@kynesyslabs/demosdk/xm-websdk" const rpc_url = "https://rpc.ankr.com/eth_sepolia" const instance = await EVM.create() ``` The create method will ping the RPC and return an instance of the SDK connected to the RPC. ### Changing the RPC URL You can assign a new rpc url as shown: ```typescript theme={null} instance.setRpc("") await instance.connect() ``` ### Reading Blockchain data You can access now acess Blockchain data using the helper methods provided by the SDK. For example, to read an address balance: ```typescript theme={null} const balance = instance.getBalance("0xa5...") ``` You can access additional methods for reading blockchain data by exploring the `provider` object inside the SDK instance. For example, to get the latest block on Ethereum: ```typescript theme={null} const block = await instance.provider.getBlock("latest") ``` The shape of the `provider` object is SDK specific. Please find the link to the API reference at the bottom of the specific crosschain SDK documentation page. ### Connecting a private key To create and sign payloads, you need to attach your private key to your SDK instance. ```typescript theme={null} const privateKey = "0xaAIf..." const wallet = await instance.connectWallet(privateKey) ``` The `connectWallet` method returns the `wallet` object that holds your private key inside the SDK instance. The shape of the `wallet` object is SDK specific. Please find the link to the API reference at the bottom of the specific crosschain SDK documentation page. You can now check the public key of your connect wallet. ```typescript theme={null} const publicKey = instance.getAddress() ``` ### Creating a single transaction You can create transaction to send funds to addresses using the provided methods. ```typescript theme={null} const destination = "0x4d..." const signedTx = await instance.prepareTransfer(destination, "0.1") ``` The `preparePay` method creates a transaction to transfer funds to a destination address. It also signs the transaction using the connected private key. It returns the payload ready to be used in a DEMOS transaction. The `prepareTransfer` method is an alias of the `preparePay` method. ### Creating multiple transactions You can also create a list of transaction to transfer funds. ```typescript theme={null} const transfers = [ { address: "0xf0...", amount: "0.1", }, { address: "0x4f...", amount: "0.25", }, ] const signedTxs = await instance.prepareTransfers(transfers) ``` The `prepareTransfers` method creates and signs a list of transaction to transfer funds in the order of appearance. ### Cleaning up When you no longer need the instance, disconnect the rpc and your wallet. ```ts theme={null} await instance.disconnect() ``` # Ibc Source: https://docs.kynesys.xyz/sdk/cross-chain/ibc Inter-Blockchain Communication Protocol (IBC) is a blockchain interoperability protocol used by 110+ chains. It enables secure, permissionless, featur... # IBC Inter-Blockchain Communication Protocol (IBC) is a blockchain interoperability protocol used by 110+ chains. It enables secure, permissionless, feature-rich cross-chain interactions. ### Creating the SDK instance I'll be using the Stargaze testnet for the examples. ```ts theme={null} import { IBC } from "@kynesyslabs/demosdk/xm-websdk" const rpc_url = "https://rpc.elgafar-1.stargaze-apis.com" const instance = await IBC.create(rpc_url) ``` The instance is now connected to the Stargaze testnet and ready to be used. ### Connecting your wallet You can connect your wallet using a mnemonic or a private key as shown below. ```ts theme={null} const mnemonic = "stumble august fancy ..." await instance.connectWallet(mnemonic, { prefix: "stars", gasPrice: "0.012ustars", }) ``` When connecting your wallet, you also need to specify the address prefix for your chain and the gas price to be used when creating transactions. ### Getting your address You can get your address using the `getAddress` method. ```ts theme={null} const address = instance.getAddress() ``` ### Preparing a transaction You can prepare a transaction using the `preparePay` method. ```ts theme={null} const tx = await instance.preparePay(address, "1", { denom: "ustars", }) ``` When creating a transaction, you need to specify the recipient address, the amount to be transferred, and the token denomination. ### Preparing multiple transactions You can prepare multiple transactions using the `preparePays` method. ```ts theme={null} const txs = await instance.preparePays( [ { address: "stars1zyxw...", amount: "1" }, { address: "stars1abcd...", amount: "1" }, ], { denom: "ustars", }, ) ``` The `preparePays` method returns an array of signed transactions ready to be sent to a DEMOS node. The transactions are signed with an incrementing nonce derived from the ledger sequence. ### Signing Messages ```javascript theme={null} const message = "Hello, world!" // Signing const signature = await instance.signMessage(message, { privateKey: "", }) console.log(signature) ``` ### Verifying signature ```javascript theme={null} // Get account public key const sep256k1HdWallet = await DirectSecp256k1HdWallet.fromMnemonic( wallets.ibc.privateKey, { prefix: "stars", // network prefix here }, ) const walletAccounts = await sep256k1HdWallet.getAccounts() const currentAccount = walletAccounts.find(account => account.address.startsWith("stars"), ) const pubKey = currentAccount?.pubkey const ibcBase64PublicKey = Buffer.from(pubKey).toString("base64") // Verifying signature const verified = await instance.verifyMessage( message, signature, ibcBase64PublicKey, ) expect(verified).toBe(true) ``` ### Cleaning up When you're done with the SDK instance, you can disconnect your wallet and RPC connection. ```ts theme={null} await instance.disconnect() ``` ### Hacking The DEMOS IBC SDK is built on top of the [CosmJS](https://github.com/cosmos/cosmjs) library, and only provides a limited set of methods to interact with the blockchain. You can access the underlying CosmJS objects to have more control over the transactions and interactions with the blockchain. Here is a list of the objects you can access: | Property | Type | Description | | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | | `instance.provider` | [StargateClient](https://cosmos.github.io/cosmjs/latest/stargate/classes/StargateClient.html) | Provides read-only access to blockchain data | | `instance.wallet` | [SigningStargateClient](https://cosmos.github.io/cosmjs/latest/stargate/classes/SigningStargateClient.html) | Allows for signing and broadcasting transactions | | `instance.signer` | [DirectSecp256k1Wallet](https://cosmos.github.io/cosmjs/latest/proto-signing/classes/DirectSecp256k1Wallet.html) or [DirectSecp256k1HdWallet](https://cosmos.github.io/cosmjs/latest/proto-signing/classes/DirectSecp256k1HdWallet.html) | Manages the private key and signing operations | #### Example To get the latest block, you can use the `getBlock` method. ```ts theme={null} const block = await instance.provider.getBlock() ``` ### API Reference [https://kynesyslabs.github.io/demosdk-api-ref/classes/xmwebsdk.IBC.html](https://kynesyslabs.github.io/demosdk-api-ref/classes/xmwebsdk.IBC.html) # Identities Source: https://docs.kynesys.xyz/sdk/cross-chain/identities You can associate a crosschain identity with a DEMOS address by creating a signature using your private key and sending it to the DEMOS network. ### Adding an Identity You can associate a crosschain identity with a DEMOS address by creating a signature using your private key and sending it to the DEMOS network. DEMOS network supports linking Web2 identities too (for example, login with Twitter or with Github). See web2 [identities](../web2/identities/) Check out how to create a signature on the "**Signing Messages**" section of the respective crosschain SDK documentation. eg. [EVM](/sdk/cross-chain/evm#signing-messages) #### 0. Imports ```typescript theme={null} import { Demos } from "@kynesyslabs/demosdk/websdk"; import { Identities } from "@kynesyslabs/demosdk/abstraction"; import { InferFromSignaturePayload } from "@kynesyslabs/demosdk/types/abstraction"; ``` #### 1. Creating the signature Connect your wallet to the XM instance and create a signature ```javascript theme={null} const xmAddress = instance.getAddress(); const message = `Link ${instance.name} address ${address} to Demos`; const signature = await instance.signMessage(message); ``` The signature will be verified on the network to prevent adding identities not owned by you #### 2. Creating the DEMOS payload The payload to add a Sepolia address would look like this: ```javascript theme={null} const payload: InferFromSignaturePayload = { method: "identity_assign_from_signature", target_identity: { chain: "eth", chainId: instance.chainId, subchain: "sepolia", isEVM: true, signature: signature, signedData: message, targetAddress: instance.getAddress(), }, }; ``` #### 3. Connecting to the network Create a new `Demos` object and connect the RPC and your private key. ```typescript theme={null} const rpc = "https://node2.demos.sh"; // connect to the DEMOS rpc const demos = new Demos(); await demos.connect(rpc); // connect private key await demos.connectWallet(mnemonic); ``` #### 4. Sending the payload Then send the identity payload to the network using the `identities` instance as shown: Use `identities.inferXmIdentity` to create a transaction that adds your identity to the blockchain. Then broadcast the transaction using `demos.broadcast`. ```typescript theme={null} const identities = new Identities(); const validityData = await identities.inferXmIdentity(demos, payload); const res = await demos.broadcast(validityData); console.log(res); // { // result: 200, // response: { message: 'Signature verified. Transaction applied.' }, // require_reply: false, // extra: { confirmationBlock: 3 } // } ``` ### Getting Identities After the `confirmationBlock` has been forged, you can retrieve all the identities associated with your DEMOS address as shown: ```typescript theme={null} const res = await identities.getIdentities(demos); console.log(res); // { // "result": 200, // "response": { // "xm": { // "evm": { // "sepolia": [ // "0x4298A9D2A573dA64102255d11d6908b7e3d89b02" // ] // } // }, // "web2": {} // }, // "require_reply": false, // "extra": null // } ``` ### Removing an Identity You can remove your crosschain identity from DEMOS as shown: ```typescript theme={null} const validityData = await identities.removeXmIdentity(demos, { chain: "evm", subchain: "sepolia", targetAddress: instance.getAddress(), // the address to remove }); const res = await demos.broadcast(validityData); console.log(res); // { // result: 200, // response: { message: 'Transaction applied, waiting for confirmation' }, // require_reply: false, // extra: { confirmationBlock: 25 } // } ``` # Multiversx Egld Source: https://docs.kynesys.xyz/sdk/cross-chain/multiversx-egld MultiversX (formerly Elrond) is a highly scalable, fast, and secure blockchain platform for distributed apps, enterprise use cases, and the new intern... # MultiversX (EGLD) MultiversX (formerly Elrond) is a highly scalable, fast, and secure blockchain platform for distributed apps, enterprise use cases, and the new internet economy. ### Creating the SDK instance To interact with the MultiversX blockchain, you first need to create an instance of the SDK. Here's how you can do it: ```ts theme={null} import { MULTIVERSX } from "@kynesyslabs/demosdk/xm-websdk" const rpc_url = "https://testnet-api.multiversx.com" const instance = await MULTIVERSX.create(rpc_url) ``` The instance is now connected to the MultiversX testnet and ready to be used. ### Connecting your wallet You can connect your wallet using a private key (keyFile) and password as shown below: ```ts theme={null} const keyFile = '{"version":4,...}' // Your keyFile JSON string const password = "your-wallet-password" await instance.connectWallet(keyFile, { password: password, }) ``` Note: In a web environment, if you don't provide a keyFile and password, the SDK will attempt to connect using the MultiversX DeFi Wallet browser extension. ### Getting your address You can get your address using the `getAddress` method: ```ts theme={null} const address = instance.getAddress() ``` ### Preparing a transaction You can prepare a transaction using the `preparePay` method: ```ts theme={null} const recipientAddress = "erd1..." const amount = "0.00001" // in EGLD const tx = await instance.preparePay(recipientAddress, amount) ``` The `preparePay` method returns a signed transaction ready to be sent to a DEMOS node. ### Preparing multiple transactions You can prepare multiple transactions using the `preparePays` method: ```ts theme={null} const transfers = [ { address: "erd1...", amount: "0.00001" }, { address: "erd1...", amount: "0.00002" }, ] const txs = await instance.preparePays(transfers) ``` The `preparePays` method returns an array of signed transactions. These transactions are signed with increasing nonces derived from the account's current nonce on the network. ### Signing messages ```javascript theme={null} const message = "Hello, world!" // Signing const signature = await instance.signMessage(message) // Verifying signature const verified = await instance.verifyMessage( message, signature, instance.getAddress(), ) expect(verified).toBe(true) ``` ### Cleaning up When you're done with the SDK instance, you can disconnect your wallet: ```ts theme={null} await instance.disconnect() ``` ### Hacking The DEMOS MultiversX SDK provides a limited set of methods to interact with the blockchain. For more advanced usage, you can access the underlying objects: | Property | Type | Description | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------- | | `instance.provider` | [`INetworkProvider`](https://github.com/multiversx/mx-sdk-js-network-providers/blob/main/src/interface.ts) | Provides access to network data | | `instance.wallet` | [`UserSigner`](https://github.com/multiversx/mx-sdk-js-wallet/blob/main/src/userSigner.ts) or [`ExtensionProvider`](https://github.com/multiversx/mx-sdk-js-extension-provider/blob/main/src/extensionProvider.ts) | Manages wallet operations | #### Example To get all the tokens owned by an address (a feature not directly implemented in the SDK): ```ts theme={null} const address = instance.getAddress() const tokens = await instance.provider.getAllEsdtTokens(new Address(address)) tokens.forEach(token => { console.log(`Token: ${token.identifier}, Balance: ${token.balance}`) }) ``` This example uses the `getAllEsdtTokens` method from the underlying `INetworkProvider`, which is not directly exposed in the DEMOS SDK. For more advanced operations, refer to the following documentation: * [MultiversX SDK Core](https://github.com/multiversx/mx-sdk-js-core) * [MultiversX SDK Network Providers](https://github.com/multiversx/mx-sdk-js-network-providers) * [MultiversX SDK Wallet](https://github.com/multiversx/mx-sdk-js-wallet) ### API Reference [https://kynesyslabs.github.io/demosdk-api-ref/classes/xmwebsdk.MULTIVERSX.html](https://kynesyslabs.github.io/demosdk-api-ref/classes/xmwebsdk.MULTIVERSX.html) # Near Source: https://docs.kynesys.xyz/sdk/cross-chain/near NEAR is a user-friendly, scalable layer-1 blockchain platform that uses a sharded proof-of-stake consensus mechanism. It features human-readable accou... # NEAR NEAR is a user-friendly, scalable layer-1 blockchain platform that uses a sharded proof-of-stake consensus mechanism. It features human-readable account names, supports smart contracts in Rust and AssemblyScript, and offers low transaction fees. ### Core Concepts 1. [Account IDs](https://docs.near.org/concepts/protocol/account-id) 2. [Access Keys](https://docs.near.org/concepts/protocol/access-keys) 3. [Anatomy of a Transaction](https://docs.near.org/concepts/protocol/transaction-anatomy) 4. [Lifecycle of a Transaction](https://docs.near.org/concepts/protocol/transaction-execution) ### Setting up your wallet Use the [Meteor Wallet](https://wallet.meteorwallet.app) to create a Near account. Then go to `Settings > Security and Recovery > Export Private Key` to copy your private key. Use the [Near Testnet Faucet](https://near-faucet.io) to airdrop fund your testnet wallet. ### Using the SDK ### Initialization Import the SDK and create a new instance: ```ts theme={null} import { NEAR } from "@kynesyslabs/demosdk/xm-websdk" const rpc_url = "https://rpc.testnet.near.org" const networkId = "testnet" const instance = await NEAR.create(rpc_url, networkId) ``` You can then use the instance to do a read operation on near. ```ts theme={null} const balance = await instance.getBalance("cwilvx.testnet") console.log(`Balance: ${balance}Ⓝ`) ``` ### Connecting your wallet To perform token transfers or other transactions, you need to connect your wallet to the SDK. You also need to provide the accountId to be associated with your private key. ```ts theme={null} await instance.connectWallet( "ed25519:4QRNiJk7A584sU3aV1xhqdbdairYJybjHJq9z78s7ntw2JVeBYybZ4r4Ec", { accountId: "cwilvx.testnet", }, ) ``` You can view the address of your connected wallet using the `getAddress` method. ```ts theme={null} const address = instance.getAddress() console.log(`Address: ${address}`) ``` ### Token transfer To create a transaction to transfer Ⓝ on near, you can use the `preparePay` or `prepareTranfer` methods: ```ts theme={null} const signedTx = await instance.preparePay("cwilvx.testnet", "1.5") ``` The signed tx is a `Uint8Array` that can be used in a XM work step. ### Signing Messages ```javascript theme={null} const message = "Hello, world!" // Signing const signature = await instance.signMessage(message) console.log(signature) // Verifying signature const verified = await instance.verifyMessage( message, signature, instance.getAddress(), ) expect(verified).toBe(true) ``` ### Creating accounts You can call the `createAccount` method to create an account, passing the accountId and the Ⓝ amount to deposit to the created account. You can optionally specify the curve to use when generating the key pair for the new account. ```ts theme={null} const { keyPair, signedTx } = await instance.createAccount( "other.me.testnet", "1", ) console.log(keyPair.getPublicKey().toString()) ``` The method returns the signed transaction for creating the new account on Near, and its key pair. ### Deleting accounts You can delete the connected account by calling the `deleteAccount` method and passing the NEAR account that will receive the remaining Ⓝ balance from the account being deleted. ```ts theme={null} const signedTx = await instance.deleteAccount("cwilvx.testnet") ``` ### Hacking To create custom transactions for the Near blockchain using the Demos Near SDK, you can access the underlying API to create transactions.
MethodDescription#
instance.actionsTransaction Actions1
instance.providerThe near class instance provided by near-api-js2
instance.walletYour connected KeyPair3
instance.signerThe signer for your connected KeyPair4
instance.signTransaction(s)Sign transactions5
### Resources Here's the converted list with markdown links: * [Near Website](https://near.org) * [Near Testnet Faucet](https://near-faucet.io) * [Docs](https://docs.near.org) * [Meteor Wallet](https://wallet.meteorwallet.app) * [Near JS API Ref](https://near.github.io/near-api-js/index.html) * [Near JS Cookbook](https://github.com/near/near-api-js/tree/master/packages/cookbook) * [Block Explorer](https://testnet.nearblocks.io) * [The NEAR Protocol Specification](https://nomicon.io) # Cross Chain Source: https://docs.kynesys.xyz/sdk/cross-chain/overview The cross chain (XM) SDKs help you create payloads to interact with other blockchains from DEMOS. These SDKs are divided into 2: # Cross Chain The cross chain (XM) SDKs help you create payloads to interact with other blockchains from DEMOS. These SDKs are divided into 2: 1. Web SDKs - For use in a browser environment 2. Local SDKs - For use in a Node JS environment Under the hood, both these SDKs extend a core SDK that contains common functionality. ### Supported Blockchains Here is a list of blockchains supported by DEMOS: 1. [EVM networks (Ethereum, etc)](/sdk/cross-chain/evm) 2. [MultiversX](/sdk/cross-chain/multiversx-egld) 3. [Solana](/sdk/cross-chain/solana) 4. [IBC networks](/sdk/cross-chain/ibc) 5. [Bitcoin](/sdk/cross-chain/btc) 6. [TEN.xyz](/sdk/cross-chain/ten) 7. [TON](/sdk/cross-chain/ton) 8. [XRPL](/sdk/cross-chain/xrpl) 9. [NEAR](/sdk/cross-chain/near) 10. [Aptos](/sdk/cross-chain/aptos) 11. [TRON](/sdk/cross-chain/tron) # Solana Source: https://docs.kynesys.xyz/sdk/cross-chain/solana Solana runs on the following core concepts: # Solana [Solana](https://solana.com/) is a "high performance" network with roots in finance, smart contracts and NFTs. Solana has roots in other areas like gaming, but those won't be covered in this guide. ### Core Concepts Solana runs on the following core concepts: 1. [Accounts](https://solana.com/docs/core/accounts) 2. [Transactions](https://solana.com/docs/core/transactions) 3. [Programs (smart contracts)](https://solana.com/docs/core/programs) #### Accounts In Solana, accounts are used to store data. Each account has a unique address which can be used to access the stored data. There are 3 types of accounts: 1. Data accounts - they store data. 2. Program accounts - they host program code 3. Native accounts - native Solana programs, eg. the System account which is responsible for creating all accounts. An account in Solana is like a file in a normal operating system, and it can contain any data as defined by a program. The account you create via a wallet is categorized as a data account owned by the System program. An account hosting your program code is a program account owned by you (your account). An account stores data that looks like this: ```ts theme={null} { data: bytes, executable: boolean, lamports: number, // balance owner: PublicKey } ``` A lamport is the smallest unit of SOL (1 SOL = 1 billion lamports) \[[Accounts - Solana Cookbook](https://solanacookbook.com/core-concepts/accounts.html#facts)] | \[[Accounts - Solana Docs](https://solana.com/docs/core/accounts)] #### Programs A program is a smart contract running on the Solana network. Programs do not store data themselves, instead they store data in acccounts. ### How Solana Handles Nonces Contrary to other networks like Ethereum, Solana does not use an integer nonce. Instead, a recent block hash is used to invalidate old transactions (those signed more than 150 blocks ago). While this works well for most cases, it makes signing offline transactions impossible. Enter durable nonces. Durable nonces are 32-byte base58 encoded strings used in place of the recent block hash when signing a transaction. Durable nonces can only be advanced by broadcasting a transaction instruction to advance it. When signing a transaction using durable nonces, the first instruction is supposed to be the advance nonce instruction. How do they work? Durable nonces requires a separate nonce account which stores the current nonce value. The account can be created using the `@solana/web3.js` sdk [as shown here](https://solana.com/developers/guides/advanced/introduction-to-durable-nonces#create-nonce-accounts). While durable nonces fix the initial problem of offline transactions, since these nonces are strings, we can't advance them locally and thus we lose the ability to sign multiple offline transactions at the same time using nonces. Since a transaction needs to be broadcasted to advance the nonce, signing multiple transactions using a durable nonce would use the same value for all transactions causing only the first to be valid. \[[More on Durable Nonces](https://solana.com/developers/guides/advanced/introduction-to-durable-nonces)] ### Setting up your wallet Install the [Phantom wallet extension](https://phantom.app/) to create a new wallet. Then export your Solana secret key by going to `Settings > Manage Accounts > {Wallet Name} > Show Private Key`. Airdrop some test SOL from the [Solana Faucet](https://solfaucet.com) for testing. > The testnet faucet is empty (at the time of this writing), so you might need to use the devnet. ### Using the Sdk The solana sdk provides a utility function to get the [rpc url](https://solana.com/rpc). ```ts theme={null} import { clusterApiUrl } from "@solana/web3.js" import { SOLANA } from "@kynesyslabs/demosdk/xm-websdk" const testnet = clusterApiUrl("testnet") // const devnet = clusterApiUrl("devnet") // const mainnet = clusterApiUrl("mainnet-beta") const instance = await SOLANA.create(testnet) ``` ### Connecting your wallet Pass the base58 representation of your private key to `.connectWallet`: ```ts theme={null} await instance.connectWallet("25WecT1ApBVs9PEpNgsgEYJEjDMG...") ``` On the browser, you can also try to connect to the [Phantom](https://phantom.app) browser extension by calling `.connectWallet` without any argument. ```ts theme={null} await instance.connectWallet() // instance.wallet: PhantomProvider ``` ### Sending SOL ```ts theme={null} const signed_tx = await instance.preparePay( "tKeYE4wtowRb8yRroZShTipE18YVnqwXjsSAoNsFU6g", "0.1", ) ``` The signed transaction is a serialized form of itself (`Uint8Array`) that can now be sent to a DEMOS node for broadcasting. ### Multi token send ```ts theme={null} const signed_txs = await instance.preparePays([ { address: "tKeYE4wtowRb8yRroZShTipE18YVnqwXjsSAoNsFU6g", amount: "1", }, { address: "CU3pJe9E3NxyBMWbTAJYjoahut9juerfmmjCin2g8ipG", amount: "2", }, ]) ``` The signed transactions will be signed using the recent block hash and therefore are only valid for the next few minutes. ### Signing messages ```javascript theme={null} const message = "Hello, world!" // Signing const signature = await instance.signMessage(message) // Verifying signature const verified = await instance.verifyMessage( message, signature, instance.getAddress(), ) expect(verified).toBe(true) ``` ## Programs To execute a program on solana, you need to have its address (program Id) and IDL. On Solana, an IDL (interface definition language) is what an ABI is to an EVM smart contract. It is a programs's public interface. It define its methods, inputs and types. ### When the IDL is available Here's an example IDL of a program running on the Solana Mainnet: [https://explorer.solana.com/address/MarBmsSgKXdrN1egZf5sqe1TMai9K1rChYNDJgjq7aD/anchor-program](https://explorer.solana.com/address/MarBmsSgKXdrN1egZf5sqe1TMai9K1rChYNDJgjq7aD/anchor-program) ```ts theme={null} // Getting the IDL const idl = await instance.getProgramIdl( "MarBmsSgKXdrN1egZf5sqe1TMai9K1rChYNDJgjq7aD", ) ``` You can use the IDL to collect and convert user input to the type specified in the IDL. #### The structure of the IDL A typical IDL looks like this: ```ts theme={null} type Idl = { version: string name: string instructions: IdlInstruction[] accounts?: IdlAccountDef[] errors?: IdlErrorCode[] types?: IdlTypeDef[] constants?: IdlConstant[] } type IdlInstruction = { name: string accounts: IdlAccount[] args: IdlField[] } type IdlAccount = { name: string isMut: boolean isSigner: boolean } ``` The `instructions` are a list of the methods defined on the program. An instruction can require arguments which are declared on the `args` property. When the instruction is reading data or modifying dadta, the data account's address is needed. If data is being modified, the private key of the account's owner is required to sign the transaction. You can determine the number of signers required by checking how many accounts have a `isSigner` flag. Invoking a program: ```ts theme={null} const programId = "MarBmsSgKXdrN1egZf5sqe1TMai9K1rChYNDJgjq7aD" const programParams = { instruction: "deposit", idl: idl, args: { lamports: 100, }, accounts: { // example account: receiver: "address", // other accounts here ... }, feePayer: publicKey, signers: [Keypair, Keypair], // all signers } const signedTx = await instance.runAnchorProgram(programId, programParams) ``` After broadcasting the transaction to the network, you can await its confirmation and then read an account data. ```ts theme={null} await instance.provider.confirmTransaction(txhash, "finalized") // account affected by program invocation const modifiedAccount = new PublicKey( "5dDANLFBcmFyFQonFcJsE9i3YqK74JPAMAiL1WXmWKSL", ) const account = await instance.fetchAccount(modifiedAccount, { idl: idl, name: "newAccount", // account name programId: "MarBmsSgKXdrN1egZf5sqe1TMai9K1rChYNDJgjq7aD", // optional }) console.log(account.data) ``` The IDL and `accountName` are needed to deserialize the account data ### When the IDL is not available When you don't have the IDL, you need to know: 1. The name of the instruction 2. The index of the instruction on the program definition 3. The proper format and type of the instruction parameters 4. The accounts needed 5. The signers needed by the instruction. With all that in place, you can do the following: ```ts theme={null} const programId = "yourProgramId" const account = new Keypair() // accounts const accounts = [ { pubkey: account.publicKey, isSigner: true, isWritable: true, }, ] // parameters const params = { space: 100, } const instructionIndex = 8 const instructionName = "ixName" // the signed tx const signedTx = await instance.runRawProgram(programId, { instructionName, instructionIndex, params, keys: accounts, feePayer: account.publicKey, signers: [account], }) ``` ## Resources 1. [The Solana Cookbook](https://solanacookbook.com/) 2. [The Solana Program Library](https://spl.solana.com/) 3. [ANCHOR - Solana Sealevel Framework](https://www.anchor-lang.com/) 4. [Javascript Client](https://solana.com/docs/clients/javascript) 5. [Known Programs \[Github\]](https://github.com/solana-labs/explorer/blob/master/app/utils/programs.ts) 6. [Interacting with Custom Programs](https://solana.com/docs/clients/javascript#interacting-with-custom-programs) ## API Reference [https://kynesyslabs.github.io/demosdk-api-ref/classes/xmwebsdk.SOLANA.html](https://kynesyslabs.github.io/demosdk-api-ref/classes/xmwebsdk.SOLANA.html) # Ten Source: https://docs.kynesys.xyz/sdk/cross-chain/ten TEN integration is in progress — the SDK class exists but is not yet usable. # TEN TEN support is in development. The `TEN` class exists in the SDK source (`src/multichain/core/ten.ts`) but is not yet exported from any package entry point, and key methods are not yet implemented: * `signMessage()` returns `"Not implemented"` * `verifyMessage()` returns `false` This page will be filled in once those methods land and the class is exported. # The Xmscript Source: https://docs.kynesys.xyz/sdk/cross-chain/the-xmscript The `XMScript` is a JSON script that dictates how a cross chain should be broadcasted to another blockchain from a DEMOS node. # The XMScript The `XMScript` is a JSON script that dictates how a cross chain should be broadcasted to another blockchain from a DEMOS node. The `XMScript` contains the following information: 1. Blockchain identifier .eg. `eth`,`solana`, `xrpl`, etc 2. Subchain identifier .eg. `sepolia` 3. Signed payload 4. Task parameters 5. Task type. eg. `pay`, `contract_read` 6. RPC URL (Optional) A single `XMScript` can contain multiple blockchain payloads. The order of execution of these payloads is controlled using an `operation_order` list. Here's an example `XMScript` ```typescript theme={null} import { XMScript } from "@kynesyslabs/demosdk/types" const xmscript: XMScript = { operations: { "0": { chain: "eth", is_evm: true, rpc: null, subchain: "sepolia", task: { params: null, signedPayloads: ["0x02f87583aa36a76a84237ef1..."], type: "pay", }, }, }, operations_order: ["0"], } ``` # Ton Source: https://docs.kynesys.xyz/sdk/cross-chain/ton The Open Network (TON) is a decentralized and open internet, created by the community using a technology designed by Telegram. # TON The Open Network (TON) is a decentralized and open internet, created by the community using a technology designed by Telegram. ### Setting up a wallet Install the [Tonkeeper](https://tonkeeper.com/) wallet extension in your browser and create your wallet. Then export your recovery phrase by going to `Settings > Recovery Phrase`. Enter your password and save your phrase. ### Creating the SDK instance You need a HTTP endpoint provider to access the TON network. We'll use the endpoint provided by [Orbs.com](https://www.orbs.com/ton-access/) for that. Install the [`@orbs-network/ton-access`](https://www.orbs.com/ton-access/) package in your project to get started. ```bash theme={null} bun add @orbs-network/ton-access ``` Then create the SDK instance as follows: ```ts theme={null} import { TON } from "@kynesyslabs/demosdk/xm-websdk" import { getHttpEndpoint } from "@orbs-network/ton-access" // Get the rpc url const endpoint = await getHttpEndpoint({ network: "testnet", }) const instance = await TON.create(endpoint) ``` ### Connecting your wallet You can connect your wallet using a mnemonic as follows: ```ts theme={null} await instance.connectWallet("wall park wife ...") ``` ### Getting your address You can get your wallet address as follows: ```ts theme={null} const address = instance.getAddress() ``` ### Creating a transaction Now you can create a signed TON transfer transaction. ```ts theme={null} const tx = await instance.preparePay( "EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N", "1", ) ``` The `tx` will be a signed [BOC](https://docs.ton.org/develop/data-formats/cell-boc) buffer that can be sent to a DEMOS node for broadcasting. ### Creating multiple transactions You can create multiple transfer transactions using the `preparePays` method. ```ts theme={null} const transfers = [ { address: "EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N", amount: "1", }, { address: "EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N", amount: "1", }, ] const txs = await instance.preparePays(transfers) ``` ### Signing Messages ```javascript theme={null} const message = "Hello, world!" // Signing const signature = await instance.signMessage(message) // Verifying signature const verified = await instance.verifyMessage( message, signature, instance.wallet.publicKey.toString("hex"), ) expect(verified).toBe(true) ``` ### Cleaning up You can remove your wallet and RPC connections as follows: ```ts theme={null} await instance.disconnect() ``` ### Hacking The DEMOS TON sdk is built on top of the [TonJs](https://github.com/ton-org/ton) library, and only provides a limited set of methods to interact with the TON blockchain. You can access the underlying TonJs objects to have more control over the transactions and interactions with the blockchain. Here is a list of the objects you can access: | Property | Type | Description | | ------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------ | | `instance.provider` | [TonClient](https://ton-community.github.io/ton/classes/TonClient.html) | Provides read-only access to blockchain data | | `instance.signer` | [KeyPair](https://ton-community.github.io/ton/classes/KeyPair.html) | Allows for signing and broadcasting transactions | | `instance.wallet` | [WalletContractV4](https://ton-community.github.io/ton/classes/WalletContractV4.html) | Manages the private key and signing operations | ### Resources 1. [Official TON Website](https://ton.org/en) 2. [Official TON Docs](https://docs.ton.org/learn/introduction) 3. [Official TON JS Client](https://github.com/ton-org/ton) 4. [Explore TON wallets](https://ton.org/en/wallets) 5. Ton web JS client 6. [Tonviewer - TON Block explore](https://testnet.tonviewer.com/) ### API Reference [https://kynesyslabs.github.io/demosdk-api-ref/classes/xmwebsdk.TON.html](https://kynesyslabs.github.io/demosdk-api-ref/classes/xmwebsdk.TON.html) # Tron Source: https://docs.kynesys.xyz/sdk/cross-chain/tron TRON is a decentralized blockchain platform that focuses on high-throughput, scalability, and content distribution. The DEMOS TRON SDK lets you prepare and sign TRX transfer payloads from a Demos transaction. # TRON TRON is a decentralized blockchain platform that focuses on high-throughput, scalability, and content distribution. The DEMOS TRON SDK lets you prepare and sign TRX transfer payloads from a Demos transaction. The SDK is built on top of [TronWeb](https://tronweb.network/docu/docs/intro/) and exposes a small set of helpers covering the common operations. ### Setting up a wallet Install the [TronLink](https://www.tronlink.org/) wallet extension or any wallet that exposes a TRON private key. You will need the raw private key (with or without a leading `0x`) to connect the SDK. ### Creating the SDK instance You need a TRON HTTP RPC endpoint. The TRON Foundation provides public mainnet and testnet endpoints; alternatively, [TronGrid](https://www.trongrid.io/) and [GetBlock](https://getblock.io/) offer hosted endpoints. ```ts theme={null} import { TRON } from "@kynesyslabs/demosdk/xm-localsdk" const instance = await TRON.create("https://api.shasta.trongrid.io") ``` ### Connecting your wallet ```ts theme={null} await instance.connectWallet("your_tron_private_key") ``` The private key may be passed with or without a leading `0x`; whitespace is trimmed automatically. ### Getting your address ```ts theme={null} const address = instance.getAddress() // returns a base58 address (e.g. TXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX) ``` ### Getting your balance ```ts theme={null} const balanceSun = await instance.getBalance(address) // balance is returned as a string in SUN // 1 TRX = 1,000,000 SUN ``` ### Creating a transaction `preparePay` produces a signed TRX transfer ready to broadcast. The amount is specified in **SUN** (the smallest TRX unit, where 1 TRX = 1,000,000 SUN): ```ts theme={null} const tx = await instance.preparePay( "TXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", // receiver (base58) "1000000", // 1 TRX, in SUN ) ``` For multiple transfers in one batch, use `preparePays`: ```ts theme={null} const txs = await instance.preparePays([ { address: "TXXXXX...", amount: "1000000" }, { address: "TYYYYY...", amount: "2000000" }, ]) ``` ### Broadcasting a transaction ```ts theme={null} const response = await instance.sendTransaction(tx) // response.result is "success" or "error" // response.hash is the txid on success ``` ### Signing messages ```ts theme={null} const message = "Hello, world!" const signature = await instance.signMessage(message) const verified = await instance.verifyMessage( message, signature, instance.getAddress(), ) ``` ### Hacking The DEMOS TRON SDK is a thin wrapper around [TronWeb](https://github.com/tronprotocol/tronweb). For advanced operations (smart contracts, TRC10/TRC20 tokens, resources, etc.) you can access the underlying TronWeb instance directly: | Property | Type | Description | | ------------------- | --------- | -------------------------------------------- | | `instance.provider` | `TronWeb` | Provides read-only access to blockchain data | | `instance.wallet` | `TronWeb` | Initialized with a private key for signing | ### Resources 1. [Official TRON Website](https://tron.network/) 2. [Official TRON Developer Docs](https://developers.tron.network/) 3. [TronWeb GitHub](https://github.com/tronprotocol/tronweb) 4. [Tronscan Block Explorer](https://tronscan.org/) 5. [Shasta Testnet Faucet](https://www.trongrid.io/shasta) ### API Reference [https://kynesyslabs.github.io/demosdk-api-ref/classes/xmwebsdk.TRON.html](https://kynesyslabs.github.io/demosdk-api-ref/classes/xmwebsdk.TRON.html) # Unstoppable Domains Source: https://docs.kynesys.xyz/sdk/cross-chain/unstoppable-domains Unstoppable Domains(UD) provides user-owned, blockchain-based domain names that serve as universal digital identities for the decentralized web. You can link your Web3 unstoppable domain as an identity using the Demos SDK by following this guide. [Unstoppable Domains](https://unstoppabledomains.com) (UD) provides user-owned, blockchain-based domain names that serve as universal digital identities for the decentralized web. You can link your Web3 unstoppable domain as an identity using the Demos SDK by following this guide. ## Initialization Initialize a new `Demos` class instance to communicate with the Demos Network: ```typescript theme={null} import { Demos } from "@kynesyslabs/demosdk/websdk"; // Connect to Demos RPC const demos = new Demos(); await demos.connect("https://node2.demos.sh"); // Connect wallet const mnemonic = demos.newMnenonic(); await demos.connectWallet(mnemonic); console.log("Demos Address:", demos.getEd25519Address()); ``` ## Connect Signing Wallet Connect the crosschain wallet to use to prove ownership of the domain. Assuming you want to sign using your ETH wallet, that would look like this: ```typescript EVM theme={null} import { EVM } from "@kynesyslabs/demosdk/xmcore"; const evm = await EVM.create("https://ethereum-rpc.publicnode.com"); // Replace with your private key or mnemonic await evm.connectWallet(ETH_PRIVATE_KEY); console.log("ETH address: ", evm.getAddress()); ``` ```typescript SOLANA theme={null} import { SOLANA } from "@kynesyslabs/demosdk/xmcore"; const solana = await SOLANA.create("https://api.mainnet-beta.solana.com"); // Replace with your private key or mnemonic await solana.connectWallet(SOLANA_PRIVATE_KEY); console.log("SOLANA address: ", solana.getAddress()); ``` You can use the domain owner address or any of the addresses added as records to sign the proof of ownership payload. You can sign using any of the [supported chains](/sdk/cross-chain/overview#supported-blockchains). ## Resolve UD Domain Lookup your domain and fetch its metadata: ```typescript theme={null} import { Identities } from "@kynesyslabs/demosdk/abstraction"; const identities = new Identities(); // Replace with your domain const resolutionData = await identities.resolveUDDomain(demos, "aladdin.demos"); console.log("Domain resolution data:", resolutionData); ``` The `resolutionData` object contains information about your domain including the owner and records attached to it. That looks something like this: ```typescript theme={null} const resolutionData = { domain: "0xbejesus.lfg", network: "polygon", registryType: "UNS", authorizedAddresses: [ // RECORDS { address: "39SHnACs1Rv3wv...", recordKey: "crypto.SOL.address", signatureType: "solana", }, { address: "39SHnACs1Rv3wv...", recordKey: "token.SOL.SOL.SOL.address", signatureType: "solana", }, // ... more records here ], metadata: { evm: { tokenId: "0x794b63bcd20b66f28744a844e0751cfbac305c3427266490e9651336d093ea49", owner: "0x0c99aDe44A554B...", resolver: "0xa9a6A3626993D487d2Dbda3173cf58cA1a9D9e9f", }, }, }; ``` Finally, use the `resolutionData` to link your domain to your Demos address: ```typescript EVM theme={null} const evmAddress = evm.getAddress(); const demosAddress = demos.getEd25519Address(); const challenge = await identities.generateUDChallenge( demosAddress, evmAddress ); const signature = await evm.signMessage(challenge); const validityData = await identities.addUnstoppableDomainIdentity( demos, // demos instance evmAddress, // evm address signature, // challenge signature challenge, // signed challenge resolutionData // domain resolution data ); console.log("Validity Data:", JSON.stringify(validityData, null, 2)); // Broadcast validity data to apply transaction if (validityData.result === 200) { const res = await demos.broadcast(validityData); console.log("Broadcast result:", JSON.stringify(res, null, 2)); } ``` ```typescript SOLANA theme={null} const solanaAddress = solana.getAddress(); const demosAddress = demos.getEd25519Address(); const challenge = await identities.generateUDChallenge( demosAddress, solanaAddress ); const signature = await solana.signMessage(challenge); const validityData = await identities.addUnstoppableDomainIdentity( demos, // demos instance solanaAddress, // solana address signature, // challenge signature challenge, // signed challenge resolutionData // domain resolution data ); console.log("Validity Data:", JSON.stringify(validityData, null, 2)); // Broadcast validity data to apply transaction if (validityData.result === 200) { const res = await demos.broadcast(validityData); console.log("Broadcast result:", JSON.stringify(res, null, 2)); } ``` The `addUnstoppableDomainIdentity` uses the connected cross-chain wallet to create a proof of ownership payload, then sends the payload to the Demos RPC for verification. ## Getting Connected UD Domains You can fetch the UD Domains connected to your account as shown: ```typescript theme={null} const domainsRes = await identities.getUDIdentities(demos); console.log("UD Domains Res:", JSON.stringify(domainsRes, null, 2)); ``` # Xrpl Source: https://docs.kynesys.xyz/sdk/cross-chain/xrpl The XRP Ledger is a decentralized cryptographic ledger powered by a network of peer-to-peer servers. It is fast, energy efficient, and reliable, with ... # XRPL The XRP Ledger is a decentralized cryptographic ledger powered by a network of peer-to-peer servers. It is fast, energy efficient, and reliable, with low transaction costs. XRP is the native cryptocurrency of the XRP Ledger, used to facilitate transactions on the network. ### Core Concepts 1. Accounts: [XRPL Accounts](https://xrpl.org/accounts.html) 2. Transactions: [Transaction Basics](https://xrpl.org/transaction-basics.html) 3. Consensus: [XRPL Consensus](https://xrpl.org/consensus.html) 4. Fees: [XRPL Fees](https://xrpl.org/fees.html) ### Setting up your wallet To interact with the XRP Ledger, you need a wallet. You can use various wallet solutions, such as: * [GemWallet](https://gemwallet.app/) For development purposes, you can generate a test wallet using the [XRPL Faucet](https://xrpl.org/xrp-testnet-faucet.html). ### Creating the SDK Instance Import the SDK and create a new instance: ```ts theme={null} import { XRPL } from "@kynesyslabs/demosdk/xm-websdk" const rpc_url = "wss://s.altnet.rippletest.net:51233" const with_reconnect = false const instance = new XRPL(rpc_url) await instance.connect(with_reconnect) ``` The `with_reconnect` parameter is optional and defaults to `true`. It is used to specify whether the SDK should attempt to reconnect to the XRPL if the web socket connection is lost. The XRPL SDK uses a web socket connection. HTTP RPCs are not supported. ### Connecting your wallet To perform transactions, connect your wallet to the SDK: ```ts theme={null} await instance.connectWallet("sEd7rBGm5kxzauR...") ``` You can view the address of your connected wallet using the `getAddress` method: ```ts theme={null} const address = instance.getAddress() console.log(`Address: ${address}`) ``` ### Getting balance To get the balance of an account: ```ts theme={null} const balance = await instance.getBalance("rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe") console.log(`Balance: ${balance} XRP`) ``` ### Token transfer To create a transaction to transfer XRP, use the `preparePay` method: ```ts theme={null} const signedTx = await instance.preparePay( "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe", "10", ) ``` The `signedTx` object contains the signed transaction that can be used in a DEMOS transaction. ### Multiple transfers To prepare multiple transfers at once, use the `preparePays` method: ```ts theme={null} const transfers = [ { address: "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe", amount: "10" }, { address: "rUCzEr6jrEyMpjhs4wSdQdz4g8Y382NxfM", amount: "20" }, ] const signedTxs = await instance.preparePays(transfers) ``` ### Signing Messages ```javascript theme={null} const message = "Hello, world!" // Signing const signature = await instance.signMessage(message) // Verifying signature const verified = await instance.verifyMessage( message, signature, instance.wallet.publicKey, ) expect(verified).toBe(true) ``` ### Advanced Usage The DEMOS XRPL SDK is built on top of the [XRPL JS Library](https://js.xrpl.org/), and provides a limited set of methods to interact with the XRP Ledger. For more advanced use cases, you can access the underlying API to have more control over the transactions and interactions with the blockchain. Here is a list of the objects you can access: | Method | Description | | ----------------------------- | ---------------------------------------------------------------------------------------- | | `instance.provider` | The [Client](https://js.xrpl.org/classes/Client.html) instance from the `xrpl` library | | `instance.wallet` | The [Wallet](https://js.xrpl.org/classes/Wallet.html) instance for the connected account | | `instance.signTransaction(s)` | Sign one or multiple transactions | | `xrplGetLastSequence` | Get the last sequence number for an address | ### API Reference [https://kynesyslabs.github.io/demosdk-api-ref/classes/xmwebsdk.XRPL.html](https://kynesyslabs.github.io/demosdk-api-ref/classes/xmwebsdk.XRPL.html) ### Resources * [XRPL Website](https://xrpl.org/) * [XRPL Testnet Faucet](https://xrpl.org/xrp-testnet-faucet.html) * [XRPL Docs](https://xrpl.org/docs.html) * [XRPL JS Library](https://js.xrpl.org/) * [XRPL Explorer (Testnet)](https://testnet.xrpl.org/) # Demoswork Source: https://docs.kynesys.xyz/sdk/demoswork DemosWork is a module that helps you create a script (DemosScript) that can be executed on the omniweb. # Demoswork DemosWork is a module that helps you create a script (DemosScript) that can be executed on the omniweb. A `DemosScript` is made up of these components: 1. Work steps - A step is a single action that can be executed on the omniweb. 2. Operations - An operation is a group of steps or a conditional. 3. The operation order - This is an ordered list of operation UIDs. It dictates the order in which the operations in a script are supposed to be executed. DemosWork is implemented as a class that looks like this: *@/demoswork/work.ts* ```ts theme={null} class DemosWork { script: DemoScript = { steps: {}, operations: {}, operationOrder: new Set(), } push(operation: DemosWorkOperation) toJSON() } ``` The `push` method adds an operation to the DemoScript. API Reference: [https://kynesyslabs.github.io/demosdk-api-ref/classes/demoswork.DemosWork.html](https://kynesyslabs.github.io/demosdk-api-ref/classes/demoswork.DemosWork.html) ## Work steps A work step is a single action that can be executed on the omniweb. It can either be a web2, xm or a native transaction. Say for example, sending tokens on Ethereum or calling a web2 API. A work step is implemented as follows: ```ts theme={null} class WorkStep { id: string = "step_" + UID context: "xm" | "web2" | "native" content: WorkStepInput // payload specific to the context output: { [key: string]: StepOutputKey } description: string timestamp: number = Date.now() critical: boolean depends_on: string[] } ``` The `critical` property is used to stop the execution of the `DemoScript` if this step fails. The `depends_on` array is a list of work steps or operations that should be executed successfully before this step is executed. The `output` of a workstep indicated above is not the result of executing the step. Since we don't have that value yet, the output is represented as objects that operations can use to refer to the future/actual output of the step. The `output` of a workstep indicated above is not the result of executing the step. Since we don't have that value yet, the output is represented as objects that operations can use to refer to the future/actual output of the step. When executing an operation that needs an output of a step, that step will be executed and its result parsed using the key specified to get the value. API Reference: [https://kynesyslabs.github.io/demosdk-api-ref/classes/demoswork.WorkStep.html](https://kynesyslabs.github.io/demosdk-api-ref/classes/demoswork.WorkStep.html) ## Operations An operation controls the execution of steps therefore enabling scripting on the omniweb. An operation can be a group of steps or a conditional. An operation is implemented as follows: ```ts theme={null} abstract class DemosWorkOperation { id: string = "op_" + UID abstract type: string steps: Record = {} operations: Set = new Set() output: any // specific to the operation type operationScript: OperationScript critical: boolean depends_on: string[] addWork(work: WorkStep | DemosWorkOperation): void } ``` The `critical` property is used to stop the execution of the `DemoScript` if a step fails. The `depends_on` array is a list of worksteps or operations that should be executed successfully before an operation is executed. The operation class indexes the steps and operations it contains in the `steps` and `operations` fields. The `operationScript` field is specific to the operation type and is copied into the main script when the operation is added to the work instance. The `addWork` method is used to add steps and operations to the operation (where applicable). If the work passed is an operation, its steps and operations are copied into the current operation. ### Conditional Operation A conditional operation now builds on top of that: *@/demoswork/operations/conditional/index.ts* ```ts theme={null} class Condition { id: string action: WorkStep | DemosWorkOperation = null value_a: Condition | Operand = null value_b: Condition | Operand = null work: Map = new Map() } class ConditionalOperation extends DemosWorkOperation { type = "conditional" override operationScript: ConditionalOperationScript = { id: this.id, operationType: "conditional", conditions: new Map(), order: [], } constructor(...conditions: Condition[]): void if(value_a, operator, value_b) then(step: WorkStep | DemosWorkOperation) else(step: WorkStep | DemosWorkOperation) } ``` A conditional operation is created by passing in conditions to the constructor. The `if`, `then` and `else` methods are helpers that can be used to build a conditional operation by chaining them together. API Reference: [https://kynesyslabs.github.io/demosdk-api-ref/classes/demoswork.ConditionalOperation.html](https://kynesyslabs.github.io/demosdk-api-ref/classes/demoswork.ConditionalOperation.html) ### Base Operation The base operation groups steps and operations together without any additional logic. The base operation is implemented as follows: *@/demoswork/operations/baseoperation.ts* ```ts theme={null} class BaseOperation extends DemosWorkOperation { type = "base" operationScript: BaseOperationScript = { id: this.id, operationType: "base", order: [], } constructor(...work: Array) override addWork(...work: Array): void } ``` Work can be added to the base operation by passing in the work to the constructor or using the `addWork` method. API Reference: [https://kynesyslabs.github.io/demosdk-api-ref/classes/demoswork.BaseOperation.html](https://kynesyslabs.github.io/demosdk-api-ref/classes/demoswork.BaseOperation.html) ## The Operation Order This is an ordered list of Operation UID strings. It dictates the order in which the operations in a script are supposed to be executed. # Getting Started Source: https://docs.kynesys.xyz/sdk/getting-started Install the latest version of the SDK with: # Getting Started ### Installation Install the latest version of the SDK with: **Recommended**: We strongly recommend using [bun](https://bun.sh) as your package manager. All examples are tested with bun. ```bash theme={null} bun add @kynesyslabs/demosdk@latest ``` ### Polyfills When working on a browser environment, you need to configure polyfills for `Buffer`. For a Vite project, this can be done using the [vite-plugin-node-polyfills](https://www.npmjs.com/package/vite-plugin-node-polyfills) plugin. ```bash theme={null} bun add --dev vite-plugin-node-polyfills ``` Then update your `vite.config.ts` to register the plugin and select polyfills. ```typescript theme={null} import { defineConfig } from "vite"; import { nodePolyfills } from "vite-plugin-node-polyfills"; export default defineConfig({ plugins: [ // ... nodePolyfills({ globals: { Buffer: true, }, }), ], }); ``` Check out the [plugin documentation](https://www.npmjs.com/package/vite-plugin-node-polyfills) for configuring other polyfills. ## Next Steps Set up wallet authentication and connect to the network Explore the full WebSDK capabilities Learn about multi-chain integrations Jump into practical code examples # Post Quantum Cryptography Source: https://docs.kynesys.xyz/sdk/post-quantum-cryptography Demos support use of the following Quantum safe signing algorithms for transaction and transmission signing: Demos support use of the following Quantum safe signing algorithms for transaction and transmission signing: 1. Falcon 2. ML-DSA You can sign transactions using Falcon by connecting your mnemonic as shown: ```typescript theme={null} import { Demos } from "@kynesyslabs/demosdk/websdk" const demos = new Demos() const mnemonic = demos.newMnemonic() await demos.connectWallet(mnemonic, { algorithm: "falcon", dual_sign: true }) const rawTx = {...} const tx = await demos.sign(rawTx) ``` ### PQC Identities You can link your PQC identities to your ed25519 address on the Demos network, to allow you to only include the PQC signature on future transactions. ```typescript theme={null} import { Identities } from "@kynesyslabs/demosdk/abstraction" const identities = new Identities() // Bind every supported PQC algorithm to the connected ed25519 address. const validityData = await identities.bindPqcIdentity(demos, "all") const res = await demos.broadcast(validityData) console.log(res) ``` You can also bind a specific subset of algorithms by passing a `PQCAlgorithm[]` array instead of `"all"`: ```typescript theme={null} // Bind only Falcon, leaving ML-DSA unbound. await identities.bindPqcIdentity(demos, ["falcon"]) ``` For future wallet connections, you can connect your wallet without the `dual_sign` option. Transactions will not have the ed25519 signature and your ownership of the ed25519 address will be validated on the network. ### Removing PQC identities To unbind PQC identities, use `removePqcIdentity`. It mirrors the binding shape — pass `"all"` to remove every algorithm, or a `PQCAlgorithm[]` to remove a subset. ```typescript theme={null} // Remove every PQC algorithm bound to the connected address. const validityData = await identities.removePqcIdentity(demos, "all") await demos.broadcast(validityData) // Or remove just one algorithm. await identities.removePqcIdentity(demos, ["falcon"]) ``` # Access Control Source: https://docs.kynesys.xyz/sdk/storage-programs/access-control Configure who can read, write, and delete a Storage Program using the v4.0.3 ACL: owner, public, and restricted modes plus allowlists, blacklists, and groups. # Access Control Every Storage Program carries a `StorageProgramACL` that the node consults on every read, write, and delete. This guide covers the ACL data model, the resolution priority the node uses to grant or deny a request, each of the three modes, and how to mutate the ACL after deployment. This page assumes you already have a connected `demos` instance and an `ownerAddress`. If not, start with [Getting Started](./getting-started). ## The ACL data model The ACL lives on the program payload as `acl` and is fully typed in `@kynesyslabs/demosdk`: ```typescript theme={null} import type { StorageProgramACL, StorageACLMode, StorageGroupPermissions, } from "@kynesyslabs/demosdk/storage" // type StorageACLMode = "owner" | "public" | "restricted" interface StorageProgramACL { /** Default behavior when no allowed/blacklist/group rule matches. */ mode: StorageACLMode /** Addresses explicitly granted access. */ allowed?: string[] /** Addresses explicitly denied. Takes precedence over allowed and groups. */ blacklisted?: string[] /** Named groups, each with members and a permission set. */ groups?: Record } interface StorageGroupPermissions { members: string[] permissions: ("read" | "write" | "delete")[] } ``` A few invariants that come straight from the SDK source: * The owner of a program (the `deployerAddress` passed to `createStorageProgram`) always has full access. They are not listed in `allowed` and cannot be put on `blacklisted`. * `mode` is the only required field. The other three (`allowed`, `blacklisted`, `groups`) are independent and can be combined freely. * If you omit `acl` entirely on `createStorageProgram`, the SDK fills in `{ mode: "owner" }`. ## Resolution priority The node walks the ACL in this exact order on every operation. The first rule that matches wins. 1. **Owner** — the program's deployer always has FULL ACCESS (read, write, delete). 2. **Blacklisted** — if the requester is in `blacklisted`, the request is DENIED, even if they also appear in `allowed` or in a group. 3. **Allowed** — if the requester is in `allowed`, the requested permission is granted. 4. **Groups** — each group is checked; if the requester is a member of a group whose `permissions` include the requested action, it's granted. 5. **Mode fallback** — when nothing above matches: * `owner` → DENIED * `public` → READ-only (write and delete are denied) * `restricted` → DENIED That ordering is what makes `blacklisted` strictly stronger than `allowed` and groups, and what makes `public` a true "anyone can read" default without needing to list every reader. ## Mode reference All three modes use the same payload-builder flow described in [Getting Started](./getting-started#step-3-sign-confirm-broadcast): build the payload, `demos.storagePrograms.sign(payload)`, `demos.confirm(tx)`, then `demos.broadcast(validityData)`. ### Owner mode Default mode. Only the program's owner can read, write, or delete. Use it for personal data you never intend to share. ```typescript theme={null} import { StorageProgram } from "@kynesyslabs/demosdk/storage" const nonce = await demos.getAddressNonce(ownerAddress) const payload = StorageProgram.createStorageProgram( ownerAddress, "personalNotes", { todo: ["draft proposal", "review PR"] }, "json", { mode: "owner" }, { nonce }, ) const tx = await demos.storagePrograms.sign(payload) await demos.broadcast(await demos.confirm(tx)) ``` Any non-owner read, write, or delete falls through to step 5 and is denied. ### Public mode Anyone can read. Only the owner can write or delete. Use it for announcements, profiles, indexes, or anything you want to publish openly. ```typescript theme={null} const payload = StorageProgram.createStorageProgram( ownerAddress, "publicProfile", { displayName: "Alice", bio: "Web3 builder" }, "json", { mode: "public" }, { nonce }, ) ``` Non-owner writes and deletes still hit the mode fallback and are denied — `public` only relaxes reads. ### Restricted mode Reads, writes, and deletes are denied by default. Access is granted only through explicit `allowed` entries or `groups`. Use it for team workspaces, multi-tenant data, or any non-public dataset with a known set of collaborators. ```typescript theme={null} const payload = StorageProgram.createStorageProgram( ownerAddress, "teamRoster", { members: [], pendingInvites: [] }, "json", { mode: "restricted", allowed: ["demos1alice...", "demos1bob..."], }, { nonce }, ) ``` A pure `{ mode: "restricted" }` with no `allowed` and no `groups` is identical to `owner` mode — only the deployer gets through. ## `allowed` and `blacklisted` These two lists are independent of `mode` and can be combined with any mode. * `allowed` grants the listed addresses all permissions (read, write, delete). * `blacklisted` denies the listed addresses every permission, no matter what `mode`, `allowed`, or `groups` say. It is checked before `allowed` and groups, so blacklisting always wins. A typical pattern: a public dataset where you've identified a few abusive addresses you want to lock out. ```typescript theme={null} const payload = StorageProgram.createStorageProgram( ownerAddress, "publicFeed", { posts: [] }, "json", { mode: "public", blacklisted: ["demos1spam...", "demos1abuser..."], }, { nonce }, ) ``` Or a restricted program where one specific address gets full access while the rest of the world is denied: ```typescript theme={null} const payload = StorageProgram.createStorageProgram( ownerAddress, "auditLog", { entries: [] }, "json", { mode: "restricted", allowed: ["demos1auditor..."], }, { nonce }, ) ``` The owner cannot be denied. Putting the owner's address in `blacklisted` has no effect — rule 1 (owner = full access) is checked before rule 2. ## Groups Groups let you express role-based access without flattening every member into a single `allowed` list. Each group has a `members` array and a `permissions` array drawn from `"read"`, `"write"`, and `"delete"`. ```typescript theme={null} const payload = StorageProgram.createStorageProgram( ownerAddress, "teamDocument", { title: "Q1 Plan", body: "" }, "json", { mode: "restricted", groups: { editors: { members: ["demos1alice...", "demos1bob..."], permissions: ["read", "write"], }, viewers: { members: ["demos1carol...", "demos1dave..."], permissions: ["read"], }, }, }, { nonce }, ) ``` In that example: * The owner can do anything (rule 1). * Alice and Bob can read and write but not delete. * Carol and Dave can read. * Anyone else falls through to the `restricted` mode fallback and is denied. A single address in multiple groups gets the union of those groups' permissions. `blacklisted` still overrides every group membership. ## Mutating the ACL after creation Use `StorageProgram.updateAccessControl(storageAddress, acl)` to replace the ACL on an existing program. **Only the program's owner can submit this transaction** — any other signer is rejected at the node. ```typescript theme={null} // Open up an internal program to the public for read-only access. const aclPayload = StorageProgram.updateAccessControl( storageAddress, { mode: "public" }, ) const tx = await demos.storagePrograms.sign(aclPayload) await demos.broadcast(await demos.confirm(tx)) ``` The `acl` argument is the new ACL in full — the node replaces the stored ACL with what you send. To add a member to a group, read the current ACL, mutate the `groups` map, and submit the whole object back: ```typescript theme={null} import type { StorageProgramACL } from "@kynesyslabs/demosdk/storage" const program = await StorageProgram.getByAddress(rpcUrl, storageAddress) if (!program) throw new Error("Program not found") // `program` does not return the ACL directly today — keep the ACL you set // on creation in your own state, or re-derive it from your application logic // before calling updateAccessControl. const nextAcl: StorageProgramACL = { mode: "restricted", groups: { editors: { members: ["demos1alice...", "demos1bob...", "demos1eve..."], permissions: ["read", "write"], }, }, } const aclPayload = StorageProgram.updateAccessControl(storageAddress, nextAcl) const tx = await demos.storagePrograms.sign(aclPayload) await demos.broadcast(await demos.confirm(tx)) ``` A few constraints worth remembering: * The owner of a program is set at creation time and cannot be changed via `updateAccessControl`. Ownership is permanent. * `updateAccessControl` is a regular Storage Program transaction — it incurs the same per-chunk fee as any other write. See [Getting Started → Fees](./getting-started#fees). * The new ACL takes effect once the transaction is included in a block. In-flight reads against the previous ACL may still succeed until the new state is committed. ## Quick decision guide | You want… | Use | | -------------------------------------------- | ------------------------------------------------------ | | Personal data only you can touch | `mode: "owner"` | | Open read, owner-only writes | `mode: "public"` | | A small fixed set of collaborators | `mode: "restricted"` + `allowed` | | Different roles with different permissions | `mode: "restricted"` + `groups` | | Block known-bad addresses | any mode + `blacklisted` | | Open to all readers but tightened for writes | `mode: "public"` (writes are owner-only by definition) | ## Convenience helpers The SDK exposes a few static helpers on `StorageProgram` that return prebuilt ACL objects so you don't have to spell them out: ```typescript theme={null} StorageProgram.privateACL() // { mode: "owner" } StorageProgram.publicACL() // { mode: "public" } StorageProgram.restrictedACL(["demos1alice...", "demos1bob..."]) // { mode: "restricted", allowed: [...] } StorageProgram.groupACL({ editors: { members: ["demos1alice..."], permissions: ["read", "write"] }, }) // { mode: "restricted", groups: {...} } StorageProgram.blacklistACL("public", ["demos1spam..."]) // { mode: "public", blacklisted: ["demos1spam..."] } ``` All of these return a `StorageProgramACL` you can pass straight into `createStorageProgram` or `updateAccessControl`. ## Programmatic permission checks If you want to predict whether a given address would be granted a permission against a given ACL — for example, to gate UI elements before submitting a transaction — use `StorageProgram.checkPermission`: ```typescript theme={null} import { StorageProgram } from "@kynesyslabs/demosdk/storage" const acl: StorageProgramACL = { mode: "restricted", groups: { editors: { members: ["demos1alice..."], permissions: ["read", "write"], }, }, blacklisted: ["demos1spam..."], } StorageProgram.checkPermission(acl, ownerAddress, ownerAddress, "delete") // true — owner always wins StorageProgram.checkPermission(acl, ownerAddress, "demos1alice...", "write") // true — granted via the editors group StorageProgram.checkPermission(acl, ownerAddress, "demos1alice...", "delete") // false — editors do not include "delete" StorageProgram.checkPermission(acl, ownerAddress, "demos1spam...", "read") // false — blacklisted, even with no other rule ``` This helper runs the exact resolution priority described above, so it's a faithful preview of what the node will do. ## Legacy access control (deprecated) Before v3.1.0, programs used a single string field `accessControl: "private" | "public" | "restricted" | "deployer-only"` plus a top-level `allowedAddresses` array. That shape is **deprecated** and only kept on `StorageProgramPayload` for backward compatibility with old clients. New code should always use `acl`. The mapping if you're migrating older payloads: | Legacy `accessControl` | New `acl.mode` | | ---------------------- | ----------------------------------------------------------- | | `"private"` | `"owner"` | | `"deployer-only"` | `"owner"` | | `"public"` | `"public"` | | `"restricted"` | `"restricted"` (move `allowedAddresses` into `acl.allowed`) | The SDK still exposes `StorageProgram.createStorageProgramLegacy` and `StorageProgram.updateAccessControlLegacy` for that legacy shape, both marked `@deprecated`. Don't call them in new code — the new `acl` form is strictly more expressive and is the only one that supports `blacklisted` and `groups`. ## Where to go next * [Operations](./operations) — full lifecycle of a Storage Program (create, write, granular updates, delete) and how the ACL applies at each step. * [RPC Queries](./rpc-queries) — passing a requester identity to ACL-gated reads and listing programs by owner. * [API Reference](./api-reference) — every `StorageProgram` static helper, payload shape, and response type. * [Cookbook](/sdk/cookbook/storage-programs/examples) — end-to-end recipes including team workspaces and public profiles. # Api Reference Source: https://docs.kynesys.xyz/sdk/storage-programs/api-reference Method-signature reference for the v4.0.3 Storage Programs SDK: every Demos accessor, StorageProgram static helper, payload type, and constant. # Storage Programs API Reference This page is the deep-link reference for the Storage Programs surface of `@kynesyslabs/demosdk@v4.0.3`. Every signature, return type, and parameter shape on this page is lifted directly from the SDK source. For the narrative walkthrough, start at [Getting Started](./getting-started). The Storage Programs surface lives in two modules: * `@kynesyslabs/demosdk/websdk` — the `Demos` class with the `storagePrograms` accessor (signing + reading). * `@kynesyslabs/demosdk/storage` — the `StorageProgram` class (payload builders, validators, ACL helpers), all type aliases, the `STORAGE_PROGRAM_CONSTANTS` object, and the type guards. ## Imports ```typescript theme={null} import { Demos } from "@kynesyslabs/demosdk/websdk" import { StorageProgram, STORAGE_PROGRAM_CONSTANTS, isStorageProgramPayload, isValidEncoding, isValidStorageLocation, } from "@kynesyslabs/demosdk/storage" import type { StorageProgramData, StorageProgramListItem, StorageProgramOperation, StorageEncoding, StorageLocation, StorageACLMode, StorageGroupPermissions, StorageProgramACL, StorageProgramPayload, StorageProgramTransactionContent, StorageProgramTransaction, } from "@kynesyslabs/demosdk/storage" ``` There is no `DemosClient` and no `demos.storageProgram` (singular). The class is `Demos` and the accessor is `demos.storagePrograms` (plural). The static helpers on `StorageProgram` (e.g. `deriveStorageAddress`, `getDataSize`) are **not** exported as standalone functions — call them on the class. ## `Demos` storage accessor The `storagePrograms` property on a connected `Demos` instance exposes two methods: ### `demos.storagePrograms.sign` Signs a Storage Program payload as a transaction. The returned `Transaction` is ready for `demos.confirm` followed by `demos.broadcast` (or `demos.broadcastAndWait`). #### Signature ```typescript theme={null} storagePrograms.sign( payload: StorageProgramPayload, ): Promise ``` #### Parameters | Parameter | Type | Description | | --------- | ----------------------- | -------------------------------------------------------------------------------------------- | | `payload` | `StorageProgramPayload` | A payload produced by one of the `StorageProgram.*` builders. Must include `storageAddress`. | #### Returns `Promise` — a fully signed transaction with `content.type = "storageProgram"` and `content.data = ["storageProgram", payload]`. #### Behavior * Throws `"Wallet not connected"` if no keypair is attached to the `Demos` instance. * Throws `"Storage address Not found in payload"` if `payload.storageAddress` is missing. * Sets `tx.content.to` to `payload.storageAddress` before signing. #### Example ```typescript theme={null} const payload = StorageProgram.createStorageProgram( demos.getAddress(), "appConfig", { version: "1.0" }, "json", { mode: "public" }, { nonce: await demos.getAddressNonce(demos.getAddress()) }, ) const tx = await demos.storagePrograms.sign(payload) const validity = await demos.confirm(tx) await demos.broadcast(validity) ``` ### `demos.storagePrograms.read` Fetches a Storage Program by address over HTTP. Issues `GET /storage-program/
`. If a wallet is connected, the request is annotated with `identity` and `signature` headers (ed25519 signature over the connected address) so ACL-protected programs can resolve permissions. #### Signature ```typescript theme={null} storagePrograms.read( address: string, ): Promise ``` #### Parameters | Parameter | Type | Description | | --------- | -------- | ---------------------------------------------------- | | `address` | `string` | The storage program address (`stor-{40 hex chars}`). | #### Returns `Promise` — see [`StorageProgramResponse`](#storageprogramresponse). The `success` field indicates whether the lookup resolved; on failure, `error` and `errorCode` are populated. #### Example ```typescript theme={null} const result = await demos.storagePrograms.read("stor-7a8b9c...") if (result.success) { console.log(result.data) // top-level data field console.log(result.metadata) // top-level metadata field console.log(result.sizeBytes) // top-level sizeBytes field } else { console.error(result.errorCode, result.error) } ``` Reads do not produce a transaction and incur no fee. The response shape is flat — fields like `data`, `metadata`, `owner`, `sizeBytes`, `createdAt`, `updatedAt` live at the top level of the response object, not nested under `data.variables` / `data.metadata`. ## `StorageProgram` static helpers All payload builders and validators are **static methods** on the `StorageProgram` class. They are pure (with the exception of the async query helpers documented under [RPC Queries](./rpc-queries)) — they build typed payloads, they do not broadcast. ### Address derivation #### `StorageProgram.deriveStorageAddress` Computes the deterministic storage address for a (deployer, programName, nonce, salt) tuple. ##### Signature ```typescript theme={null} static deriveStorageAddress( deployerAddress: string, programName: string, nonce: number, salt?: string, ): string ``` ##### Parameters | Parameter | Type | Required | Description | | ----------------- | -------- | -------- | ------------------------------------------------------------------------------------------------------ | | `deployerAddress` | `string` | yes | The address that will own the program. | | `programName` | `string` | yes | Human-readable name of the program. | | `nonce` | `number` | yes | Sender's account nonce — guarantees a unique address per transaction even when `programName` collides. | | `salt` | `string` | no | Optional extra entropy. Defaults to `""`. | ##### Returns `string` — `stor-` followed by the first 40 characters of `sha256(":::")`. ##### Example ```typescript theme={null} const address = StorageProgram.deriveStorageAddress( "demos1abc...", "myConfig", 42, "salt123", ) // "stor-7a8b9c..." (40 hex chars after the "stor-" prefix) ``` ### Program operations #### `StorageProgram.createStorageProgram` Builds a `CREATE_STORAGE_PROGRAM` payload. The storage address is derived inside the helper from `(deployerAddress, programName, options.nonce, options.salt)`. ##### Signature ```typescript theme={null} static createStorageProgram( deployerAddress: string, programName: string, data: Record | string, encoding?: StorageEncoding, acl?: Partial, options?: { nonce: number salt?: string metadata?: Record storageLocation?: StorageLocation }, ): StorageProgramPayload ``` ##### Parameters | Parameter | Type | Required | Description | | ----------------- | ----------------------------------------------- | -------- | ---------------------------------------------------------------------------------- | | `deployerAddress` | `string` | yes | The creator's address; becomes the program owner. | | `programName` | `string` | yes | Unique-per-deployer-per-nonce program name. | | `data` | `Record \| string` | yes | Initial payload — JSON object for `"json"` encoding, base64 string for `"binary"`. | | `encoding` | `StorageEncoding` | no | `"json"` (default) or `"binary"`. | | `acl` | `Partial` | no | Access control. Missing fields default to `{ mode: "owner" }`. | | `options` | `{ nonce; salt?; metadata?; storageLocation? }` | yes | `nonce` is **required**. | ##### Returns `StorageProgramPayload` with `operation: "CREATE_STORAGE_PROGRAM"` and `storageLocation` defaulting to `"onchain"` when omitted. ##### Throws * `"nonce is required for storage program creation"` if `options.nonce` is `undefined`. ##### Example ```typescript theme={null} // JSON storage with public read access const payload = StorageProgram.createStorageProgram( "demos1abc...", "appConfig", { version: "1.0", features: ["auth", "storage"] }, "json", { mode: "public" }, { nonce: 42 }, ) // Binary storage with group ACL and metadata const binary = StorageProgram.createStorageProgram( "demos1abc...", "teamDocument", base64EncodedPdf, "binary", { mode: "restricted", groups: { editors: { members: ["demos1a...", "demos1b..."], permissions: ["read", "write"], }, viewers: { members: ["demos1c..."], permissions: ["read"], }, }, }, { nonce: 43, metadata: { filename: "report.pdf", mimeType: "application/pdf" }, }, ) ``` #### `StorageProgram.writeStorage` Builds a `WRITE_STORAGE` payload that **replaces** the entire `data` field of an existing program. For surgical updates, use the granular helpers below. ##### Signature ```typescript theme={null} static writeStorage( storageAddress: string, data: Record | string, encoding?: StorageEncoding, ): StorageProgramPayload ``` ##### Parameters | Parameter | Type | Required | Description | | ---------------- | ------------------------------- | -------- | ------------------------------------------------ | | `storageAddress` | `string` | yes | The `stor-{hash}` address. | | `data` | `Record \| string` | yes | New payload (must match the program's encoding). | | `encoding` | `StorageEncoding` | no | `"json"` (default) or `"binary"`. | ##### Returns `StorageProgramPayload` with `operation: "WRITE_STORAGE"`. ##### Example ```typescript theme={null} const payload = StorageProgram.writeStorage( "stor-7a8b9c...", { newKey: "value", existingKey: "updatedValue" }, "json", ) ``` #### `StorageProgram.readStorage` Builds a `READ_STORAGE` payload **for transaction validation only**. This does **not** perform an actual read — use [`demos.storagePrograms.read`](#demos-storageprograms-read) (or one of the [RPC query helpers](./rpc-queries)) to fetch program data. ##### Signature ```typescript theme={null} static readStorage(storageAddress: string): StorageProgramPayload ``` ##### Returns `StorageProgramPayload` with `operation: "READ_STORAGE"` — useful when you need to construct a payload object for validation pipelines or type-guards. ##### Example ```typescript theme={null} // Validation only — does NOT fetch data const payload = StorageProgram.readStorage("stor-7a8b9c...") // Real read: const data = await demos.storagePrograms.read("stor-7a8b9c...") ``` #### `StorageProgram.updateAccessControl` Builds an `UPDATE_ACCESS_CONTROL` payload. Owner-only at execution time. ##### Signature ```typescript theme={null} static updateAccessControl( storageAddress: string, acl: Partial, ): StorageProgramPayload ``` ##### Parameters | Parameter | Type | Required | Description | | ---------------- | ---------------------------- | -------- | ---------------------------------------------------------------------------------------- | | `storageAddress` | `string` | yes | The program address. | | `acl` | `Partial` | yes | New ACL configuration. The full ACL is replaced — to keep existing fields, include them. | ##### Returns `StorageProgramPayload` with `operation: "UPDATE_ACCESS_CONTROL"`. ##### Example ```typescript theme={null} // Make the program public StorageProgram.updateAccessControl("stor-7a8b9c...", { mode: "public" }) // Block specific addresses while staying public StorageProgram.updateAccessControl("stor-7a8b9c...", { mode: "public", blacklisted: ["demos1bad...", "demos1spam..."], }) // Group-based access StorageProgram.updateAccessControl("stor-7a8b9c...", { mode: "restricted", groups: { admins: { members: ["demos1admin..."], permissions: ["read", "write", "delete"] }, users: { members: ["demos1u1...", "demos1u2..."], permissions: ["read"] }, }, }) ``` #### `StorageProgram.deleteStorageProgram` Builds a `DELETE_STORAGE_PROGRAM` payload. Irreversible at execution time; the program and all stored data are removed. ##### Signature ```typescript theme={null} static deleteStorageProgram(storageAddress: string): StorageProgramPayload ``` ##### Returns `StorageProgramPayload` with `operation: "DELETE_STORAGE_PROGRAM"`. ##### Example ```typescript theme={null} const payload = StorageProgram.deleteStorageProgram("stor-7a8b9c...") ``` ### Granular field operations These helpers build payloads for in-place edits to JSON-encoded programs. Each is rejected by the chain when applied to a binary-encoded program. #### `StorageProgram.setField` Sets a single top-level field to a value. ##### Signature ```typescript theme={null} static setField( storageAddress: string, field: string, value: unknown, ): StorageProgramPayload ``` ##### Returns `StorageProgramPayload` with `operation: "SET_FIELD"`, plus `field` and `value`. ##### Example ```typescript theme={null} StorageProgram.setField("stor-7a8b9c...", "theme", "dark") ``` #### `StorageProgram.setItem` Replaces an item at `index` inside an array field. Negative indexing is supported (`-1` = last element). ##### Signature ```typescript theme={null} static setItem( storageAddress: string, field: string, index: number, value: unknown, ): StorageProgramPayload ``` ##### Returns `StorageProgramPayload` with `operation: "SET_ITEM"`, plus `field`, `index`, and `value`. ##### Example ```typescript theme={null} // Replace the first user StorageProgram.setItem("stor-7a8b9c...", "users", 0, { name: "Updated", role: "admin" }) // Replace the last user StorageProgram.setItem("stor-7a8b9c...", "users", -1, { name: "Last", role: "user" }) ``` #### `StorageProgram.appendItem` Appends an item to an array field. ##### Signature ```typescript theme={null} static appendItem( storageAddress: string, field: string, value: unknown, ): StorageProgramPayload ``` ##### Returns `StorageProgramPayload` with `operation: "APPEND_ITEM"`, plus `field` and `value`. ##### Example ```typescript theme={null} StorageProgram.appendItem("stor-7a8b9c...", "users", { name: "New", role: "viewer" }) ``` #### `StorageProgram.deleteField` Removes a top-level field from the stored object entirely. ##### Signature ```typescript theme={null} static deleteField( storageAddress: string, field: string, ): StorageProgramPayload ``` ##### Returns `StorageProgramPayload` with `operation: "DELETE_FIELD"`, plus `field`. ##### Example ```typescript theme={null} StorageProgram.deleteField("stor-7a8b9c...", "legacyConfig") ``` #### `StorageProgram.deleteItem` Removes the item at `index` from an array field. Negative indexing is supported. The array is compacted after removal. ##### Signature ```typescript theme={null} static deleteItem( storageAddress: string, field: string, index: number, ): StorageProgramPayload ``` ##### Returns `StorageProgramPayload` with `operation: "DELETE_ITEM"`, plus `field` and `index`. ##### Example ```typescript theme={null} // Remove the first item StorageProgram.deleteItem("stor-7a8b9c...", "users", 0) // Remove the last item StorageProgram.deleteItem("stor-7a8b9c...", "users", -1) ``` ### Validators #### `StorageProgram.validateSize` Returns `true` when the data fits within `STORAGE_PROGRAM_CONSTANTS.MAX_SIZE_BYTES` (1 MB). ##### Signature ```typescript theme={null} static validateSize( data: Record | string, encoding?: StorageEncoding, ): boolean ``` ##### Example ```typescript theme={null} if (!StorageProgram.validateSize(data, "json")) { throw new Error("Payload exceeds 1 MB") } ``` #### `StorageProgram.getDataSize` Returns the size of the data in bytes. For `"binary"` encoding, the input is treated as base64 and the **decoded** byte length is returned. For `"json"` encoding, `JSON.stringify` is run and the UTF-8 byte length of the result is returned. ##### Signature ```typescript theme={null} static getDataSize( data: Record | string, encoding?: StorageEncoding, ): number ``` ##### Example ```typescript theme={null} const size = StorageProgram.getDataSize({ key: "value" }, "json") console.log(`Data size: ${size} bytes`) ``` #### `StorageProgram.validateNestingDepth` Returns `true` when the JSON object's nesting depth is `<= maxDepth`. JSON-only. ##### Signature ```typescript theme={null} static validateNestingDepth( data: any, maxDepth?: number, ): boolean ``` | Parameter | Type | Default | Description | | ---------- | -------- | --------------------------------------------------------- | ------------------------------ | | `data` | `any` | — | Object to inspect. | | `maxDepth` | `number` | `STORAGE_PROGRAM_CONSTANTS.MAX_JSON_NESTING_DEPTH` (`64`) | Maximum allowed nesting depth. | ##### Example ```typescript theme={null} if (!StorageProgram.validateNestingDepth(data)) { throw new Error("JSON nesting too deep (>64 levels)") } ``` #### `StorageProgram.calculateStorageFee` Computes the storage fee for a payload. Pricing is **1 DEM per 10 KB chunk, minimum 1 DEM**, returned as a `bigint` in OS (1 DEM = 10⁹ OS). ##### Signature ```typescript theme={null} static calculateStorageFee( data: Record | string, encoding?: StorageEncoding, ): bigint ``` ##### Returns `bigint` — fee in OS. Use `denomination.osToDem` (from `@kynesyslabs/demosdk`) to convert to whole DEM for display. ##### Example ```typescript theme={null} import { denomination } from "@kynesyslabs/demosdk" const feeOs = StorageProgram.calculateStorageFee({ key: "value" }, "json") console.log(`Storage fee: ${denomination.osToDem(feeOs)} DEM`) // Reference points: // 5 KB -> 1 DEM // 15 KB -> 2 DEM // 100 KB -> 10 DEM // 1 MB -> 103 DEM ``` ## Type reference ### `StorageProgramPayload` The single payload shape produced by every `StorageProgram.*` builder. Different operations populate different subsets of fields. ```typescript theme={null} interface StorageProgramPayload { operation: StorageProgramOperation storageAddress: string /** Required for CREATE_STORAGE_PROGRAM */ programName?: string /** "json" (default) or "binary" */ encoding?: StorageEncoding /** * For json encoding: Record (max 64 nesting levels) * For binary encoding: base64 encoded string * Max size: 1 MB. */ data?: Record | string /** Robust ACL: owner / public / restricted, plus allowed/blacklisted/groups */ acl?: StorageProgramACL /** @deprecated Use `acl` instead. */ accessControl?: StorageProgramAccessControl /** @deprecated Use `acl.allowed` instead. */ allowedAddresses?: string[] /** Optional salt mixed into address derivation */ salt?: string /** Optional free-form metadata (filename, mimeType, description, ...) */ metadata?: Record /** "onchain" (default). "ipfs" is reserved and currently falls back to onchain. */ storageLocation?: StorageLocation // Granular operation fields (JSON encoding only) /** Required for SET_FIELD / SET_ITEM / APPEND_ITEM / DELETE_FIELD / DELETE_ITEM */ field?: string /** Required for SET_ITEM and DELETE_ITEM */ index?: number /** Required for SET_FIELD / SET_ITEM / APPEND_ITEM */ value?: unknown } ``` ### `StorageProgramACL` ```typescript theme={null} interface StorageProgramACL { /** Default behavior when no other rule matches */ mode: StorageACLMode /** Addresses explicitly allowed to read/write */ allowed?: string[] /** Addresses explicitly blocked (wins over allowed/groups) */ blacklisted?: string[] /** Named groups with members and per-group permissions */ groups?: Record } ``` ACL resolution order (highest priority first): 1. Owner — full access, always. 2. Blacklisted — denied, regardless of allowed/groups. 3. `allowed` — full permissions granted. 4. `groups` — per-group permission check. 5. `mode` fallback — `"owner"` and `"restricted"` deny; `"public"` allows read. ### `StorageGroupPermissions` ```typescript theme={null} interface StorageGroupPermissions { members: string[] permissions: ("read" | "write" | "delete")[] } ``` ### `StorageACLMode` ```typescript theme={null} type StorageACLMode = "owner" | "public" | "restricted" ``` | Mode | Default behavior (no other rule matched) | | -------------- | -------------------------------------------------- | | `"owner"` | Owner only. Most restrictive. | | `"public"` | Anyone reads, only the owner writes. | | `"restricted"` | Only addresses in `allowed` / `groups` may access. | ### `StorageEncoding` ```typescript theme={null} type StorageEncoding = "json" | "binary" ``` * `"json"` — `data` is `Record`. * `"binary"` — `data` is a base64 string. ### `StorageLocation` ```typescript theme={null} type StorageLocation = "onchain" | "ipfs" ``` `"ipfs"` is reserved for future hybrid storage and is **not yet implemented** — payloads with `storageLocation: "ipfs"` currently fall back to `"onchain"`. ### `StorageProgramOperation` ```typescript theme={null} type StorageProgramOperation = | "CREATE_STORAGE_PROGRAM" | "WRITE_STORAGE" | "READ_STORAGE" | "UPDATE_ACCESS_CONTROL" | "DELETE_STORAGE_PROGRAM" | "SET_FIELD" | "SET_ITEM" | "APPEND_ITEM" | "DELETE_FIELD" | "DELETE_ITEM" ``` `READ_STORAGE` is a query operation — it is not submitted as a transaction in normal usage. ### `StorageProgramTransactionContent` ```typescript theme={null} type StorageProgramTransactionContent = Omit & { type: "storageProgram" data: ["storageProgram", StorageProgramPayload] } ``` ### `StorageProgramTransaction` ```typescript theme={null} interface StorageProgramTransaction extends Omit { content: StorageProgramTransactionContent } ``` ### `StorageProgramData` Returned by node-level RPC queries (e.g. `StorageProgram.getByAddress`). ```typescript theme={null} interface StorageProgramData { storageAddress: string owner: string programName: string encoding: "json" | "binary" data?: Record | string | null metadata?: Record | null storageLocation: string sizeBytes: number createdAt: string updatedAt: string /** Transaction hash that created this storage program */ createdByTx?: string /** Transaction hash of the last modification */ lastModifiedByTx?: string /** All transaction hashes that touched this program */ interactionTxs?: string[] } ``` ### `StorageProgramListItem` ```typescript theme={null} interface StorageProgramListItem { storageAddress: string programName: string encoding: "json" | "binary" sizeBytes: number storageLocation: string createdAt: string updatedAt: string } ``` ### `StorageProgramResponse` The response shape returned by `demos.storagePrograms.read`. All payload fields are top-level — there is no nested `data.variables` / `data.metadata` envelope. ```typescript theme={null} interface StorageProgramResponse { success: boolean storageAddress?: string owner?: string programName?: string encoding?: "json" | "binary" data?: Record | string | null metadata?: Record | null storageLocation?: string sizeBytes?: number createdAt?: string updatedAt?: string error?: string errorCode?: string } ``` ### `StorageFieldType` Returned by the `getFieldType` RPC helper. ```typescript theme={null} type StorageFieldType = | "string" | "number" | "boolean" | "array" | "object" | "null" | "undefined" ``` ### `StorageProgramFieldsResponse` ```typescript theme={null} interface StorageProgramFieldsResponse { fields: string[] count: number } ``` ## Constants ### `STORAGE_PROGRAM_CONSTANTS` ```typescript theme={null} import { STORAGE_PROGRAM_CONSTANTS } from "@kynesyslabs/demosdk/storage" ``` | Constant | Value | Meaning | | ------------------------ | ------------------------------- | ------------------------------------------------------------ | | `MAX_SIZE_BYTES` | `1_048_576` (1 MB) | Hard cap on payload size for both JSON and binary encodings. | | `PRICING_CHUNK_BYTES` | `10_240` (10 KB) | Granularity for fee calculation. | | `FEE_PER_CHUNK` | `OS_PER_DEM` (`10⁹` OS = 1 DEM) | Fee per chunk, expressed as a `bigint` in OS. | | `MAX_JSON_NESTING_DEPTH` | `64` | Maximum nesting depth for JSON encoding. | The object is declared `as const`, so its fields are read-only. ## Type guards Runtime narrowing helpers exported from `@kynesyslabs/demosdk/storage`. ### `isStorageProgramPayload` ```typescript theme={null} function isStorageProgramPayload(payload: unknown): payload is StorageProgramPayload ``` Returns `true` when `payload` is a non-null object with a `string` `storageAddress` and a `string` `operation` matching one of the values in [`StorageProgramOperation`](#storageprogramoperation). ### `isValidEncoding` ```typescript theme={null} function isValidEncoding(encoding: unknown): encoding is StorageEncoding ``` Returns `true` only for `"json"` or `"binary"`. ### `isValidStorageLocation` ```typescript theme={null} function isValidStorageLocation(location: unknown): location is StorageLocation ``` Returns `true` only for `"onchain"` or `"ipfs"`. ## Deprecated members These exist for backward compatibility with pre-v3 storage programs and should not be used in new code. | Deprecated | Replacement | | ------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- | | `StorageProgram.createStorageProgramLegacy(...)` | `StorageProgram.createStorageProgram(..., acl, options)` | | `StorageProgram.updateAccessControlLegacy(...)` | `StorageProgram.updateAccessControl(address, acl)` | | `StorageProgramAccessControl` (`"private" \| "public" \| "restricted" \| "deployer-only"`) | `StorageACLMode` (`"owner" \| "public" \| "restricted"`) inside `StorageProgramACL` | | `StorageProgramPayload.accessControl` | `StorageProgramPayload.acl.mode` | | `StorageProgramPayload.allowedAddresses` | `StorageProgramPayload.acl.allowed` | The legacy `"private"` and `"deployer-only"` modes both map to the modern `mode: "owner"`. ## Cross-references * [Getting Started](./getting-started) — end-to-end walkthrough (connect, build, sign, broadcast, read). * [Operations](./operations) — when to use create vs. write vs. granular ops, and the full lifecycle. * [Access Control](./access-control) — `owner` / `public` / `restricted`, allowlists, blacklists, groups, and resolution order. * [RPC Queries](./rpc-queries) — node-level queries such as `StorageProgram.getByAddress`, `getByOwner`, `searchByName`, `getFields`, `getValue`, `getItem`, `hasField`, `getFieldType`, `getAll`. * [Cookbook: Storage Programs](/sdk/cookbook/storage-programs/examples) — recipes for public profiles, team workspaces, and binary attachments. # Getting Started Source: https://docs.kynesys.xyz/sdk/storage-programs/getting-started Create, write to, and read from your first Storage Program against the v4.0.3 SDK. # Getting Started with Storage Programs This guide walks you through creating a Storage Program, writing data to it, and reading it back over RPC. ## Prerequisites * [Bun](https://bun.sh) (latest) installed * The Demos SDK: `bun add @kynesyslabs/demosdk@latest` * A Demos wallet with enough balance to cover storage fees ([1 DEM per 10 KB chunk](#fees), minimum 1 DEM) * Access to a Demos Network RPC node ## The mental model Every Storage Program operation follows the same four-step shape: 1. **Build a typed payload** with one of the `StorageProgram.*` static helpers (`createStorageProgram`, `writeStorage`, `updateAccessControl`, `deleteStorageProgram`, plus the granular `setField` / `setItem` / `appendItem` / `deleteField` / `deleteItem`). 2. **Sign** it with `demos.storagePrograms.sign(payload)` — this returns a signed `Transaction`. 3. **Confirm** gas with `demos.confirm(tx)`. 4. **Broadcast** with `demos.broadcast(validityData)` (or `demos.broadcastAndWait` for deterministic inclusion). Reads are different — they go through `demos.storagePrograms.read(address)` directly, no transaction required. ## Step 1: Connect ```typescript theme={null} import { Demos } from "@kynesyslabs/demosdk/websdk" import { StorageProgram } from "@kynesyslabs/demosdk/storage" const demos = new Demos() await demos.connect("https://node2.demos.sh") await demos.connectWallet(process.env.MNEMONIC!) const ownerAddress = demos.getAddress() // sync, returns string ``` `demos.getAddress()` is synchronous and returns the connected wallet's address as a string. It does not return a Promise. ## Step 2: Build the create payload `StorageProgram.createStorageProgram(deployer, programName, data, encoding, acl?, options)` builds the payload for the create transaction. The deterministic storage address is derived inside the helper — you don't compute it separately. ```typescript theme={null} const nonce = await demos.getAddressNonce(ownerAddress) const payload = StorageProgram.createStorageProgram( ownerAddress, "userProfile", { displayName: "Alice", joinedAt: Date.now(), stats: { posts: 0, followers: 0 }, }, "json", { mode: "public" }, // anyone reads, only owner writes { nonce, salt: "v1" }, // nonce is required; salt is optional ) console.log("storageAddress:", payload.storageAddress) // stor-{40 hex chars} — derived from sha256(deployer:programName:nonce:salt) ``` `options.nonce` is required. It is the sender's current account nonce, fetched via `demos.getAddressNonce(ownerAddress)`. Without it the helper throws `"nonce is required for storage program creation"`. The nonce is mixed into the address derivation so each create from the same deployer produces a unique address even when `programName` collides. ## Step 3: Sign, confirm, broadcast ```typescript theme={null} const tx = await demos.storagePrograms.sign(payload) const validityData = await demos.confirm(tx) // Option A — fire-and-forget; returns when the node accepts the tx. const res = await demos.broadcast(validityData) // Option B — wait for inclusion in a block. const { broadcast, status } = await demos.broadcastAndWait(validityData) if (status.state !== "included") { throw new Error(`Storage program not included; ended at ${status.state}`) } ``` See [Broadcasting a Transaction](/sdk/websdk/transactions/broadcasting-a-transaction) for the difference between `broadcast` and `broadcastAndWait` and the error types each can throw. ## Step 4: Write data `writeStorage` replaces the entire `data` field of the program. For surgical updates use the [granular operations](#granular-updates) below. ```typescript theme={null} const writePayload = StorageProgram.writeStorage( payload.storageAddress, { displayName: "Alice", bio: "Web3 builder", stats: { posts: 1, followers: 5 }, }, "json", ) const writeTx = await demos.storagePrograms.sign(writePayload) await demos.broadcast(await demos.confirm(writeTx)) ``` ## Step 5: Read data Reads are served directly over RPC — no transaction, no fee. ```typescript theme={null} const result = await demos.storagePrograms.read(payload.storageAddress) if (result.success) { console.log("data:", result.data) // your stored payload console.log("metadata:", result.metadata) // optional metadata console.log("size:", result.sizeBytes) } else { console.error(result.error, result.errorCode) } ``` The full response shape is `StorageProgramResponse` from `@kynesyslabs/demosdk/storage`. Notable fields: `owner`, `programName`, `encoding`, `data`, `metadata`, `storageLocation`, `sizeBytes`, `createdAt`, `updatedAt`. See [RPC Queries](./rpc-queries) for queries beyond the address-based lookup. ## Granular updates For JSON-encoded programs, you can change individual fields and array elements without rewriting the whole document: ```typescript theme={null} // Set a single field const a = StorageProgram.setField(payload.storageAddress, "bio", "Updated bio") // Append to an array const b = StorageProgram.appendItem(payload.storageAddress, "posts", { id: 7 }) // Replace an array element (negative indices supported, -1 = last) const c = StorageProgram.setItem(payload.storageAddress, "posts", 0, { id: 8 }) // Delete a field const d = StorageProgram.deleteField(payload.storageAddress, "legacyConfig") // Delete an array element by index const e = StorageProgram.deleteItem(payload.storageAddress, "posts", -1) ``` Each returns a `StorageProgramPayload` you sign + confirm + broadcast like any other write. Granular operations are JSON-only — they are rejected for binary-encoded programs. ## Updating access control and deleting ```typescript theme={null} // Tighten access to the owner only const aclPayload = StorageProgram.updateAccessControl( payload.storageAddress, { mode: "owner" }, ) // Remove the program permanently (owner / ACL-permissioned only) const delPayload = StorageProgram.deleteStorageProgram(payload.storageAddress) ``` Both return payloads that follow the same sign / confirm / broadcast flow. ## Fees Storage Programs are billed at **1 DEM per 10 KB chunk**, minimum 1 DEM per write. Calculate the fee for a payload before sending it: ```typescript theme={null} import { denomination } from "@kynesyslabs/demosdk" const feeOs = StorageProgram.calculateStorageFee( { displayName: "Alice", stats: { posts: 0 } }, "json", ) console.log(`fee: ${denomination.osToDem(feeOs)} DEM`) ``` The fee is returned as a `bigint` in OS (1 DEM = 10⁹ OS). See [Amounts & Denominations](/sdk/websdk/transactions/denominations) for the conversion helpers. ## Resource limits | Limit | Value | Constant | | ---------------------- | ---------------------- | -------------------------------------------------- | | Max payload size | 1 MB (1,048,576 bytes) | `STORAGE_PROGRAM_CONSTANTS.MAX_SIZE_BYTES` | | Max JSON nesting depth | 64 | `STORAGE_PROGRAM_CONSTANTS.MAX_JSON_NESTING_DEPTH` | | Pricing chunk | 10 KB (10,240 bytes) | `STORAGE_PROGRAM_CONSTANTS.PRICING_CHUNK_BYTES` | | Fee per chunk | 1 DEM (= 10⁹ OS) | `STORAGE_PROGRAM_CONSTANTS.FEE_PER_CHUNK` | ```typescript theme={null} import { STORAGE_PROGRAM_CONSTANTS } from "@kynesyslabs/demosdk/storage" if (!StorageProgram.validateSize(myData, "json")) { throw new Error(`Payload exceeds ${STORAGE_PROGRAM_CONSTANTS.MAX_SIZE_BYTES} bytes`) } if (!StorageProgram.validateNestingDepth(myData)) { throw new Error("JSON nesting too deep (>64 levels)") } ``` ## Where to go next * [Operations](./operations) — when to use create vs. write vs. granular ops, and the full lifecycle. * [Access Control](./access-control) — `owner` / `public` / `restricted` modes, allowlists, blacklists, and group permissions. * [RPC Queries](./rpc-queries) — full read-side surface, including the node-level RPC endpoints for listing, searching, and field-level lookups. * [API Reference](./api-reference) — every `StorageProgram` static helper, payload shape, and response interface. * [Cookbook](/sdk/cookbook/storage-programs/examples) — end-to-end recipes (public profile, team workspace, binary attachments). # Operations Source: https://docs.kynesys.xyz/sdk/storage-programs/operations When to use each Storage Program operation, the pre-flight checks each one needs, and how it slots into the build / sign / confirm / broadcast flow. # Storage Program Operations This guide explains **when** to reach for each Storage Program operation in an end-to-end workflow. The mechanics (build payload → sign → confirm → broadcast) are covered in [Getting Started](./getting-started); full method signatures, payload shapes, and response types live in the [API Reference](./api-reference). Recipe-style end-to-end examples live in the [Storage Program Cookbook](/sdk/cookbook/storage-programs/examples). Every write below assumes the same skeleton: ```typescript theme={null} import { Demos } from "@kynesyslabs/demosdk/websdk" import { StorageProgram } from "@kynesyslabs/demosdk/storage" const demos = new Demos() await demos.connect("https://node2.demos.sh") await demos.connectWallet(process.env.MNEMONIC!) // 1. Build payload via a StorageProgram.* static helper // 2. const tx = await demos.storagePrograms.sign(payload) // 3. const validityData = await demos.confirm(tx) // 4. await demos.broadcast(validityData) // or demos.broadcastAndWait ``` Reads bypass the transaction lifecycle entirely — they go through `demos.storagePrograms.read(address)`. ## Operation overview | Operation | Transaction? | Who can run it | Typical use | | ---------------------------------------- | ------------ | ------------------------------------------------------ | -------------------------------------------------------------- | | `CREATE_STORAGE_PROGRAM` | Yes | Any funded address (becomes owner) | Provision a new `stor-` address and seed initial data | | `WRITE_STORAGE` | Yes | Owner, `allowed` addresses, group members with `write` | Replace the entire `data` field (full snapshots, binary blobs) | | `SET_FIELD` / `SET_ITEM` / `APPEND_ITEM` | Yes | Owner, `allowed`, group members with `write` | Surgical JSON updates without rewriting the document | | `DELETE_FIELD` / `DELETE_ITEM` | Yes | Owner, `allowed`, group members with `write` | Remove a single field or array element from a JSON program | | `READ_STORAGE` | No (RPC) | Determined by ACL mode + allowed/groups | Fetch program data via `demos.storagePrograms.read(address)` | | `UPDATE_ACCESS_CONTROL` | Yes | Owner only | Change ACL mode, allowlist, blacklist, or groups | | `DELETE_STORAGE_PROGRAM` | Yes | Owner, or group members with `delete` permission | Permanently remove the program and its data | The string identifiers above are the values of the `StorageProgramOperation` enum exported from `@kynesyslabs/demosdk/storage`. You rarely write them by hand — the `StorageProgram.*` helpers fill them in. ## CREATE — provision a new program Use `StorageProgram.createStorageProgram` to mint a new deterministic `stor-` address and (optionally) seed initial data. The address is derived inside the helper from `sha256(deployer:programName:nonce:salt)`, so you don't need to call `deriveStorageAddress` yourself unless you want to know the address before signing. ```typescript theme={null} const ownerAddress = demos.getAddress() const nonce = await demos.getAddressNonce(ownerAddress) const payload = StorageProgram.createStorageProgram( ownerAddress, "userProfile", { displayName: "Alice", stats: { posts: 0 } }, "json", // encoding: "json" | "binary" { mode: "public" }, // ACL: { mode, allowed?, blacklisted?, groups? } { nonce, salt: "v1" }, // options.nonce is required ) const tx = await demos.storagePrograms.sign(payload) await demos.broadcast(await demos.confirm(tx)) ``` ### Pre-flight checks * **Nonce.** `options.nonce` is **required**. Fetch the sender's current nonce with `demos.getAddressNonce(ownerAddress)` — without it the helper throws `"nonce is required for storage program creation"`. The nonce is mixed into address derivation so two creates from the same deployer with the same `programName` produce different addresses. * **ACL choice.** Pick the mode you want now — you can change it later with `UPDATE_ACCESS_CONTROL`, but until you do, the default is `"owner"` (only the owner can read or write). * **Encoding.** `"json"` for structured `Record`, `"binary"` for base64-encoded blobs. Granular operations (`setField`, `appendItem`, etc.) are **rejected** on `"binary"` programs. * **Size.** Validate against the 1 MB ceiling before signing: ```typescript theme={null} if (!StorageProgram.validateSize(myData, "json")) { throw new Error("Payload exceeds MAX_SIZE_BYTES (1 MB)") } if (!StorageProgram.validateNestingDepth(myData)) { throw new Error("JSON nesting exceeds MAX_JSON_NESTING_DEPTH (64)") } ``` * **Fee budget.** `StorageProgram.calculateStorageFee(data, encoding)` returns a `bigint` of the fee in **OS** (1 DEM = 10⁹ OS). Storage is billed at 1 DEM per 10 KB chunk, minimum 1 DEM. * **Predict the address up front (optional).** ```typescript theme={null} const addr = StorageProgram.deriveStorageAddress(ownerAddress, "userProfile", nonce, "v1") ``` `storageLocation` defaults to `"onchain"`. The `"ipfs"` value is reserved for future hybrid storage and currently falls back to `"onchain"` on the node side — treat it as a forward-looking flag, not a working feature. ## WRITE — replace the full document Use `StorageProgram.writeStorage` when you want to overwrite the entire `data` field in one shot. This is the right choice for: * **Binary programs.** Granular operations don't apply — every update is a full replacement. * **Full snapshots.** You're computing a new state from scratch (e.g. importing from another source) and don't need to reason about diffs. * **Atomic multi-field rewrites.** When you want every changed field to land in a single transaction even though there are no granular `WRITE_MANY` semantics. ```typescript theme={null} const writePayload = StorageProgram.writeStorage( storageAddress, { displayName: "Alice", bio: "Web3 builder", stats: { posts: 1 } }, "json", ) const tx = await demos.storagePrograms.sign(writePayload) await demos.broadcast(await demos.confirm(tx)) ``` `WRITE_STORAGE` replaces `data` wholesale — it does not deep-merge. If you only want to change a couple of keys in a large JSON document, prefer the granular operations below. ## Granular operations — surgical JSON updates For JSON-encoded programs, five granular helpers let you change individual fields and array elements without rewriting (or paying to re-store) the whole document: | Helper | Operation | Use when | | --------------------------------------------------- | -------------- | --------------------------------------------------------------------------- | | `StorageProgram.setField(addr, field, value)` | `SET_FIELD` | Setting / replacing a single top-level field | | `StorageProgram.setItem(addr, field, index, value)` | `SET_ITEM` | Replacing one element of an array (negative indices supported, `-1` = last) | | `StorageProgram.appendItem(addr, field, value)` | `APPEND_ITEM` | Pushing onto an array field | | `StorageProgram.deleteField(addr, field)` | `DELETE_FIELD` | Removing a top-level field entirely | | `StorageProgram.deleteItem(addr, field, index)` | `DELETE_ITEM` | Removing one array element by index | ```typescript theme={null} const a = StorageProgram.setField(storageAddress, "bio", "Updated bio") const b = StorageProgram.appendItem(storageAddress, "posts", { id: 7 }) const c = StorageProgram.setItem(storageAddress, "posts", -1, { id: 8 }) const d = StorageProgram.deleteField(storageAddress, "legacyConfig") const e = StorageProgram.deleteItem(storageAddress, "posts", 0) // Each returns a StorageProgramPayload — same sign / confirm / broadcast flow. ``` **When to prefer granular ops over `writeStorage`:** * You're editing a small slice of a large document — fee is computed on the operation payload, not the whole stored document. * You want concurrent writers each touching different fields without overwriting each other's changes. * You want the transaction to fail loudly if the field doesn't have the shape you expect (e.g. `setItem` on a non-array). **When to fall back to `writeStorage`:** * The program is `"binary"` — granular operations are rejected for binary encoding. * You're rewriting most of the document anyway. * You need cross-field invariants (multiple fields must change atomically as one logical unit; granular ops are one-op-per-tx). ## READ — fetch program data over RPC Reads do **not** go through the transaction lifecycle. Use the RPC accessor directly: ```typescript theme={null} const result = await demos.storagePrograms.read(storageAddress) if (result.success) { console.log(result.data) // the stored JSON object or base64 string console.log(result.metadata) // optional metadata console.log(result.sizeBytes) } else { console.error(result.error, result.errorCode) } ``` The response shape is `StorageProgramResponse` from `@kynesyslabs/demosdk/storage`. If the wallet is connected, the SDK automatically attaches an `identity`/`signature` header so the node can apply ACL rules to the requester. `StorageProgram.readStorage(address)` exists as a payload builder, but it's intended for transaction-validation flows — for normal reads use `demos.storagePrograms.read(address)`. For listings, by-owner queries, name search, and field-level lookups (`getFields`, `getValue`, `getItem`, `hasField`, `getFieldType`, `getAll`) see [RPC Queries](./rpc-queries). ## UPDATE ACCESS CONTROL — owner-only ACL changes Only the program owner can change the ACL. Submit a full ACL object — it replaces the existing one rather than merging. ```typescript theme={null} const aclPayload = StorageProgram.updateAccessControl( storageAddress, { mode: "restricted", allowed: ["demos1user1...", "demos1user2..."], blacklisted: ["demos1spam..."], groups: { editors: { members: ["demos1ed1..."], permissions: ["read", "write"] }, viewers: { members: ["demos1view..."], permissions: ["read"] }, }, }, ) const tx = await demos.storagePrograms.sign(aclPayload) await demos.broadcast(await demos.confirm(tx)) ``` ### Pre-flight checks * **Owner-only.** Anyone else gets denied at the ACL check before the transaction lands. There is no "deputy" or co-owner concept. * **Mode is required.** Valid values are `"owner"` (default — only the owner can read or write), `"public"` (anyone reads, only the owner / allowed / writers in groups can write), and `"restricted"` (only `allowed` and matching group members get any access). * **Blacklist beats allowlist.** An address that appears in both `blacklisted` and `allowed` (or in a group) is denied. Use this to revoke without rebuilding the allow set. * **Opening up data.** Going from `"owner"` or `"restricted"` to `"public"` exposes everything currently stored — audit the data first if there's any chance of secrets. The convenience helpers `StorageProgram.publicACL()`, `privateACL()`, `restrictedACL(allowed)`, `groupACL(groups)`, and `blacklistACL(mode, blacklisted, allowed?)` all return ready-made `StorageProgramACL` objects. Full coverage in [Access Control](./access-control). The legacy `accessControl: "private" | "public" | "restricted" | "deployer-only"` field on the payload (and the `createStorageProgramLegacy` / `updateAccessControlLegacy` helpers) is kept for backward compatibility but is **deprecated**. Use the `acl` object with `mode` for all new code. ## DELETE — irreversible teardown ```typescript theme={null} const delPayload = StorageProgram.deleteStorageProgram(storageAddress) const tx = await demos.storagePrograms.sign(delPayload) await demos.broadcast(await demos.confirm(tx)) ``` ### Pre-flight checks * **Permission.** The owner can always delete. Other addresses can delete **only** if they are members of an ACL group with the `"delete"` permission. * **Irreversible.** There is no undo, no soft delete, no archival snapshot. The address itself remains derivable from `(deployer, programName, nonce, salt)`, but the data is gone. * **Downstream cleanup.** Invalidate caches, notify collaborators, and remove references from indexes before submitting the delete — by the time the transaction lands the data is unrecoverable. ## Resource limits at a glance | Limit | Value | Constant on `STORAGE_PROGRAM_CONSTANTS` | | ---------------------- | ---------------------- | --------------------------------------- | | Max payload size | 1 MB (1,048,576 bytes) | `MAX_SIZE_BYTES` | | Max JSON nesting depth | 64 | `MAX_JSON_NESTING_DEPTH` | | Pricing chunk size | 10 KB (10,240 bytes) | `PRICING_CHUNK_BYTES` | | Fee per chunk | 1 DEM (= 10⁹ OS) | `FEE_PER_CHUNK` | ```typescript theme={null} import { STORAGE_PROGRAM_CONSTANTS } from "@kynesyslabs/demosdk/storage" ``` ## Cross-references * [Getting Started](./getting-started) — the four-step build / sign / confirm / broadcast flow with a worked example. * [Access Control](./access-control) — `owner` / `public` / `restricted` modes, allowlists, blacklists, and group permissions. * [RPC Queries](./rpc-queries) — listings, by-owner lookups, name search, and granular field-level reads via `StorageProgram.getFields`, `getValue`, `getItem`, `hasField`, `getFieldType`, and `getAll`. * [API Reference](./api-reference) — every `StorageProgram` static method, the full `StorageProgramPayload` schema, ACL types, and the `StorageProgramResponse` shape. * [Cookbook — Storage Programs](/sdk/cookbook/storage-programs/examples) — end-to-end recipes for public profiles, team workspaces, and binary attachments. # Storage Programs Source: https://docs.kynesys.xyz/sdk/storage-programs/overview Storage Programs are a powerful key-value storage solution built into the Demos Network, providing developers with decentralized, persistent data stor... # Storage Programs (SDK) ## Introduction Storage Programs are a powerful key-value storage solution built into the Demos Network, providing developers with decentralized, persistent data storage with flexible access control. Think of Storage Programs as smart, programmable databases that live on the blockchain with built-in permission systems. ## Quick Links * [Getting Started](./getting-started) * [Operations Guide](./operations) * [Access Control](./access-control) * [RPC Queries](./rpc-queries) * [API Reference](./api-reference) * [Cookbook Recipes](/sdk/cookbook/storage-programs/overview) * [Backend Architecture](/backend/storage-programs/overview) ## What are Storage Programs? A Storage Program is a deterministic storage container that allows you to: * **Store arbitrary data**: Store any JSON-serializable data (objects, arrays, primitives) * **Control access**: Choose who can read and write your data * **Use deterministic addresses**: Predict storage addresses before creation * **Query efficiently**: Read data via RPC without transaction costs * **Update atomically**: All writes are atomic and consensus-validated ## Key Features * **Flexible access control**: Three modes (`owner`, `public`, `restricted`) cover private, collaborative, and public scenarios, plus allowlists, blacklists, and groups. See the [Access Control guide](./access-control) for details. * **Deterministic addressing**: `stor-` identifiers are derived from deployer inputs, so clients can pre-compute addresses before deployment. * **Consensus-backed writes**: Mutations are validated via consensus while reads are served instantly over RPC. * **Developer-friendly API**: High-level SDK helpers plus raw RPC endpoints give you control at the right abstraction. * **Predictable limits**: 1MB payload per program, 64 levels of nesting. ## How Storage Programs Work ### Architecture ``` ┌─────────────────────────────────────────────────────────┐ │ Client Application │ │ (Create, Write, Read, Update Access, Delete) │ └─────────────────────────┬───────────────────────────────┘ │ ┌─────────┴─────────┐ │ │ Write Operations Read Operations (RPC) │ │ ▼ ▼ ┌───────────────────────┐ ┌───────────────────────┐ │ Transaction System │ │ Query System │ │ (Consensus) │ │ (Direct DB) │ └───────┬───────────────┘ └───────┬───────────────┘ │ │ ▼ ▼ ┌──────────────────────────────────────────────────┐ │ Demos Network Global Chain Registry │ │ (GCR Database) │ │ │ │ GCR_Main.data = { │ │ variables: { ...your data... }, │ │ metadata: { programName, deployer, ... } │ │ } │ └──────────────────────────────────────────────────┘ ``` ### Data Storage Storage Programs are stored in the `GCR_Main` table's `data` column (JSONB): ```json theme={null} { "variables": { "username": "alice", "score": 1000, "settings": { "theme": "dark", "notifications": true } }, "metadata": { "programName": "myApp", "deployer": "0xdeployer123...", "accessControl": "public", "allowedAddresses": [], "created": 1706745600000, "lastModified": 1706745600000, "size": 2048 } } ``` ## Use Cases Looking for implementation-ready patterns? Jump to the [Storage Program Cookbook](../cookbook/storage-programs/examples) for end-to-end recipes covering announcements, team workspaces, user profiles, and more. This overview keeps the focus on concepts so each scenario has a single canonical home. ## Core Concepts ### Address Derivation Storage Program addresses are **deterministic** and **predictable**: ```typescript theme={null} import { StorageProgram } from '@kynesyslabs/demosdk/storage' // Generate address client-side const address = StorageProgram.deriveStorageAddress( deployerAddress, // Your wallet address programName, // Unique name: "myApp" nonce, // Sender's account nonce (required) salt // Optional salt for uniqueness: "v1" ) // Result: "stor-a1b2c3d4e5f6..." (45 characters) ``` **Format**: `stor-` + 40 hex characters ### Operations Lifecycle 1. **CREATE**: Initialize new storage program with optional data 2. **WRITE**: Replace the entire `data` field (does not merge; use granular ops for partial updates) 3. **READ**: Query data via RPC (no transaction needed) 4. **UPDATE\_ACCESS\_CONTROL**: Change access mode or allowed addresses (deployer only) 5. **DELETE**: Remove entire storage program (deployer only) ### Access Control Modes Each program chooses one of three modes (`owner`, `public`, `restricted`), optionally combined with allowlists, blacklists, and groups. Rather than duplicating the full matrix here, see the [Access Control guide](./access-control) for the authoritative table, example flows, and security checklists. ### Storage Limits These limits ensure blockchain efficiency: ```typescript theme={null} import { STORAGE_PROGRAM_CONSTANTS } from '@kynesyslabs/demosdk/storage' // STORAGE_PROGRAM_CONSTANTS = { // MAX_SIZE_BYTES: 1048576, // 1MB total // MAX_JSON_NESTING_DEPTH: 64, // 64 levels of nested objects // PRICING_CHUNK_BYTES: 10240, // 10KB fee chunk // FEE_PER_CHUNK: OS_PER_DEM, // 1 DEM per chunk // } ``` **Size Calculation**: ```typescript theme={null} const size = new TextEncoder().encode(JSON.stringify(data)).length ``` ## Security Considerations ### Data Privacy * **Private/Deployer-Only modes**: Data is stored on blockchain but access-controlled * **Encryption recommended**: For sensitive data, encrypt before storing * **Public mode**: Anyone can read - never store secrets ### Access Control * **Deployer verification**: All operations verify deployer signature * **Allowed addresses**: Restricted mode checks whitelist * **Admin operations**: Only deployer can update access or delete ### Best Practices ✅ **DO**: * Use descriptive program names for easy identification * Encrypt sensitive data before storing * Use public mode for truly public data * Test with small data first * Add salt for multiple programs with same name ❌ **DON'T**: * Store private keys or secrets unencrypted * Exceed 1MB limit (transaction will fail) * Use deeply nested objects (>64 levels) * Store data that changes very frequently (high transaction costs) ## Performance Characteristics ### Write Operations * **Latency**: Consensus time (\~2-5 seconds) * **Cost**: Transaction fee required * **Throughput**: Limited by block production * **Validation**: Full validation before inclusion ### Read Operations * **Latency**: Less than 100ms (direct database query) * **Cost**: Free (no transaction needed) * **Throughput**: Unlimited (RPC queries) * **Consistency**: Eventually consistent with blockchain state ### Storage Efficiency * **Overhead**: \~200 bytes metadata per program * **Compression**: JSONB compression in PostgreSQL * **Indexing**: Efficient JSONB queries * **Scalability**: Horizontal scaling with database ## Getting Started Ready to build with Storage Programs? Head to the [Getting Started](./getting-started) guide for your first Storage Program. ## Next Steps * [Getting Started](./getting-started) – create your first Storage Program * [Operations](./operations) – explore CRUD and access updates * [Access Control](./access-control) – configure permissions safely * [RPC Queries](./rpc-queries) – build fast read paths * [Cookbook Recipes](../cookbook/storage-programs/examples) – apply patterns in real apps * [API Reference](./api-reference) – look up method signatures # Rpc Queries Source: https://docs.kynesys.xyz/sdk/storage-programs/rpc-queries Read Storage Programs over RPC — when to use the SDK helper vs. the lower-level rpcCall, and what each of the 9 node endpoints does. # RPC Queries Storage Program reads are served over plain RPC. They cost nothing, require no transaction, and typically come back in well under 100 ms. This page covers the read surface only — for write flow see [Operations](./operations). ## Two read paths | You want | Use | | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | | The full program for a known address | `demos.storagePrograms.read(address)` — the SDK shortcut | | Anything else (list by owner, search, field-level lookup, type/exists checks) | `demos.rpcCall({ method, params })` against one of the 9 node endpoints below | The SDK does **not** wrap the granular endpoints. There is no `demos.storagePrograms.list()`, `.search()`, or `.getField()`. To call those, drop down to `demos.rpcCall(...)`. The SDK's `read` helper attaches the wallet's identity automatically (via `signMessage`) when a wallet is connected. ACL is enforced server-side: anonymous callers see only public programs; restricted/owner programs require the `requesterAddress` (passed implicitly by `read`, explicitly by `rpcCall`) to be in the ACL. ## Reading a single program ```typescript theme={null} import { Demos } from "@kynesyslabs/demosdk/websdk" const demos = new Demos() await demos.connect("https://node2.demos.sh") await demos.connectWallet(process.env.MNEMONIC!) // optional; needed for ACL-gated reads const result = await demos.storagePrograms.read("stor-7a8b9c...") if (!result.success) { console.error(result.error, result.errorCode) } else { console.log("owner: ", result.owner) console.log("name: ", result.programName) console.log("encoding: ", result.encoding) console.log("data: ", result.data) console.log("metadata: ", result.metadata) console.log("size: ", result.sizeBytes, "bytes") console.log("updated: ", result.updatedAt) } ``` Under the hood this performs `GET /storage-program/
` and, when a wallet is connected, attaches `identity: ed25519:
` and `signature: ` headers. The node uses those headers to resolve the requester for ACL. ### `StorageProgramResponse` shape This is the actual response interface from `@kynesyslabs/demosdk/storage`: ```typescript theme={null} interface StorageProgramResponse { success: boolean storageAddress?: string owner?: string programName?: string encoding?: "json" | "binary" data?: Record | string | null metadata?: Record | null storageLocation?: string sizeBytes?: number createdAt?: string // ISO 8601 updatedAt?: string // ISO 8601 error?: string errorCode?: string } ``` There is no `data.variables` or `data.metadata` nesting. `data`, `metadata`, `owner`, `programName`, `encoding`, `sizeBytes`, `createdAt`, `updatedAt` all live at the top level of `StorageProgramResponse`. For binary-encoded programs `data` is a base64-encoded string; for JSON programs it is a `Record`. ### Read-time access control `success: false` plus an `errorCode` is how access denials surface. The SDK does not throw on ACL rejection — it returns a structured response. Always branch on `result.success` before reading `result.data`. ## The 9 node RPC endpoints All of these are reached via `demos.rpcCall({ method, params: [{ message, data, muid }] })`. The `message` is the endpoint name; the `data` object carries the parameters. Most endpoints accept an optional `requesterAddress` for ACL — pass `demos.getAddress()` when you have a connected wallet and want non-public programs to resolve. `rpcCall` returns an `RPCResponse` with shape `{ result: number, response: any, require_reply: boolean, extra: any }`. `result` is an HTTP-style status code (200 on success, 400/403/404/500 on failure). The actual payload lives on `response`. When a program is missing or soft-deleted, the node returns `200 / null` (not 404) for single-program reads. ### 1. `getStorageProgram` — full program by address Same data the SDK's `read` helper returns, but reachable via `rpcCall` if you'd rather not go through the HTTP route. ```typescript theme={null} const res = await demos.rpcCall({ method: "nodeCall", params: [{ message: "getStorageProgram", data: { storageAddress: "stor-7a8b9c...", requesterAddress: demos.getAddress(), // optional; required for non-public ACL }, muid: `storage-${Date.now()}`, }], }) if (res.result === 200 && res.response) { const program = res.response // StorageProgramData console.log(program.programName, program.data) } ``` The `response` is `StorageProgramData` (or `null`) — a slightly richer cousin of `StorageProgramResponse` that adds `createdByTx`, `lastModifiedByTx`, and `interactionTxs[]`. ### 2. `getStorageProgramAll` — full program (alias) Despite the name, this is **not** a "list everything" endpoint. It takes a `storageAddress` and returns full `StorageProgramData` for that one program — kept as an alias of `getStorageProgram` for SDK back-compat. Same params, same response shape. ```typescript theme={null} const res = await demos.rpcCall({ method: "nodeCall", params: [{ message: "getStorageProgramAll", data: { storageAddress: "stor-7a8b9c...", requesterAddress: demos.getAddress() }, muid: `storage-${Date.now()}`, }], }) ``` ### 3. `getStorageProgramsByOwner` — list by owner (paginated) Server-side pagination is pushed into SQL — `limit` and `offset` apply before deserialization, so very large owner sets remain cheap. ACL is filtered in the same SQL pass: callers who are not the owner see only programs they have read access to. ```typescript theme={null} const res = await demos.rpcCall({ method: "nodeCall", params: [{ message: "getStorageProgramsByOwner", data: { owner: "demos1abc...", requesterAddress: demos.getAddress(), // optional limit: 50, // default 100, clamped to [1, 200] offset: 0, // default 0 }, muid: `storage-${Date.now()}`, }], }) const programs = res.response as Array<{ storageAddress: string programName: string encoding: "json" | "binary" sizeBytes: number storageLocation: string createdAt: string updatedAt: string }> // StorageProgramListItem[] ``` The response is always an array — never `null`. Items are `StorageProgramListItem`, the lighter shape (no `data`, no `metadata`, no `owner` — `owner` is implied because you queried for it). The default `limit` is 100 today and is documented to drop in a future release. Pass an explicit `limit` if you depend on a particular page size. ### 4. `getStorageProgramFields` — top-level field names JSON-only. Returns the keys of the top-level object. ```typescript theme={null} const res = await demos.rpcCall({ method: "nodeCall", params: [{ message: "getStorageProgramFields", data: { storageAddress: "stor-7a8b9c...", requesterAddress: demos.getAddress() }, muid: `storage-${Date.now()}`, }], }) const { fields, count } = res.response as { fields: string[]; count: number } // { fields: ["displayName", "stats", "posts"], count: 3 } ``` This matches the `StorageProgramFieldsResponse` interface exported from `@kynesyslabs/demosdk/storage`. Binary-encoded programs return `400 / INVALID_FIELD_TYPE`. ### 5. `getStorageProgramFieldType` — JSON type of a single field JSON-only. Returns one of `"string" | "number" | "boolean" | "array" | "object" | "null" | "undefined"` (`StorageFieldType`). ```typescript theme={null} const res = await demos.rpcCall({ method: "nodeCall", params: [{ message: "getStorageProgramFieldType", data: { storageAddress: "stor-7a8b9c...", field: "posts", requesterAddress: demos.getAddress(), }, muid: `storage-${Date.now()}`, }], }) const { field, type } = res.response as { field: string; type: StorageFieldType } // { field: "posts", type: "array" } ``` Field-not-found returns `404 / FIELD_NOT_FOUND`. ### 6. `getStorageProgramValue` — single field value JSON-only. Returns the field's value plus its inferred JSON type. ```typescript theme={null} const res = await demos.rpcCall({ method: "nodeCall", params: [{ message: "getStorageProgramValue", data: { storageAddress: "stor-7a8b9c...", field: "displayName", requesterAddress: demos.getAddress(), }, muid: `storage-${Date.now()}`, }], }) const { field, value, type } = res.response as { field: string value: unknown type: StorageFieldType } ``` This is the closest equivalent to "read a single key" — but be explicit about it. There is no `demos.storagePrograms.read(address, key)` overload. ### 7. `getStorageProgramItem` — array element by index JSON-only. Supports negative indexing (`-1` is the last item). ```typescript theme={null} const res = await demos.rpcCall({ method: "nodeCall", params: [{ message: "getStorageProgramItem", data: { storageAddress: "stor-7a8b9c...", field: "posts", index: -1, // negative indexing supported requesterAddress: demos.getAddress(), }, muid: `storage-${Date.now()}`, }], }) const { field, index, value, arrayLength } = res.response as { field: string index: number // resolved (non-negative) index actually used value: unknown arrayLength: number } ``` Out-of-bounds returns `400 / INDEX_OUT_OF_BOUNDS`. Field exists but is not an array returns `400 / INVALID_FIELD_TYPE`. ### 8. `hasStorageProgramField` — existence check JSON-only. For binary or non-object data the node returns `exists: false` rather than an error — it treats non-object data as having no fields. ```typescript theme={null} const res = await demos.rpcCall({ method: "nodeCall", params: [{ message: "hasStorageProgramField", data: { storageAddress: "stor-7a8b9c...", field: "apiKey", requesterAddress: demos.getAddress(), }, muid: `storage-${Date.now()}`, }], }) const { field, exists } = res.response as { field: string; exists: boolean } ``` Use this instead of `getStorageProgramValue` when you only need to know whether a field is present — it never throws on missing fields. ### 9. `searchStoragePrograms` — name search with ACL Partial match by default. ACL is filtered at the SQL layer, so anonymous callers transparently see only public programs and paginated results are full pages (no post-fetch JS filter that would silently shorten them). ```typescript theme={null} const res = await demos.rpcCall({ method: "nodeCall", params: [{ message: "searchStoragePrograms", data: { query: "config", options: { exactMatch: false, // default false — partial match limit: 25, // default 100, clamped to [1, 200] offset: 0, // default 0 }, requesterAddress: demos.getAddress(), // optional }, muid: `storage-${Date.now()}`, }], }) const programs = res.response as StorageProgramListItem[] ``` Response is always an array. Items are the `StorageProgramListItem` shape. ## Higher-level convenience: `StorageProgram` static helpers If you'd rather not assemble `nodeCall` envelopes yourself, the `StorageProgram` class exposes a parallel set of static `async` query methods that wrap the same RPC endpoints. They take an `rpcUrl` directly and return strongly-typed responses (or `null` on error). See [API Reference](./api-reference) for the full list — `StorageProgram.getByAddress`, `getByOwner`, `searchByName`, `getFields`, `getValue`, `getItem`, `hasField`, `getFieldType`, `getAll`. ```typescript theme={null} import { StorageProgram } from "@kynesyslabs/demosdk/storage" const programs = await StorageProgram.getByOwner( "https://node2.demos.sh", "demos1abc...", demos.getAddress(), // identity for ACL ) ``` These helpers do not need a connected `Demos` instance — just an RPC URL — which makes them convenient for read-only/server-side code. ## Performance & batching Reads are RPC-only: no transaction, no fee, no consensus round-trip. Typical latency is sub-100 ms against a healthy node. ### Parallelize independent reads ```typescript theme={null} const [profile, settings, posts] = await Promise.all([ demos.storagePrograms.read(profileAddress), demos.storagePrograms.read(settingsAddress), demos.storagePrograms.read(postsAddress), ]) ``` ### Use `updatedAt` as a staleness signal `StorageProgramResponse.updatedAt` is the canonical "did anything change?" field — it's an ISO 8601 timestamp set on every successful write. Cache aggressively against it rather than polling the full document: ```typescript theme={null} let cached: StorageProgramResponse | null = null async function readIfChanged(address: string) { const head = await demos.storagePrograms.read(address) if (!head.success) return cached if (cached?.updatedAt === head.updatedAt) return cached cached = head return head } ``` There is no metadata-only HEAD endpoint today, so the cheapest "did it change?" probe is still `read(address)` and a comparison against your cached `updatedAt`. ### Field-level reads avoid full-document transfer When you only need one key out of a 500 KB program, use `getStorageProgramValue` (or `StorageProgram.getValue`) instead of `read` — the node serializes only the requested field, not the whole document. ## Error handling | Surface | How it shows up | | --------------------------------------- | --------------------------------------------------------------- | | Program not found / soft-deleted | `success: false` (or `result.response === null` over `rpcCall`) | | ACL denial | `success: false` with an `errorCode` | | Wrong encoding for a JSON-only endpoint | `400 / INVALID_FIELD_TYPE` | | Field missing | `404 / FIELD_NOT_FOUND` | | Array index out of bounds | `400 / INDEX_OUT_OF_BOUNDS` | | Bad params | `400` with `error` describing the missing/invalid field | | Server error | `500 / INTERNAL_ERROR` | The SDK helpers (`storagePrograms.read`, `StorageProgram.getByAddress`, etc.) never throw on these — they return a structured response (or `null`). For full payload details on individual error codes, see node docs. ## See also * [Getting Started](./getting-started) — create / write / read flow end-to-end * [Operations](./operations) — write-side surface (granular updates, deletes, ACL changes) * [Access Control](./access-control) — how `owner` / `public` / `restricted` modes interact with read ACL * [API Reference](./api-reference) — every static helper, payload, and response interface * [Cookbook](/sdk/cookbook/storage-programs/examples) — end-to-end recipes # Utilities Source: https://docs.kynesys.xyz/sdk/utilities Helper functions exported from @kynesyslabs/demosdk/utils — sleep, address validation, hex/object conversion, and Uint8Array Base64 encoding. # Utilities The `@kynesyslabs/demosdk/utils` subpath exposes small pure-function helpers used across the SDK. They are all framework-free and safe to import in browser or Node code. ## Sleep ```typescript theme={null} import { sleep } from "@kynesyslabs/demosdk/utils" await sleep(1500) // wait 1.5 seconds ``` `sleep(ms)` returns a `Promise` that resolves after the given number of milliseconds. ## Address validation ```typescript theme={null} import { validateEd25519Address } from "@kynesyslabs/demosdk/utils" validateEd25519Address("0x1234...64hex...") // true / false ``` `validateEd25519Address(address)` returns `true` when `address` matches `/^0x[0-9a-f]{64}$/i` — a `0x` prefix followed by exactly 64 hex characters (case-insensitive). Use it to fail fast on malformed input before constructing transactions. ## Uint8Array ↔ Base64 ```typescript theme={null} import { serializeUint8Array, deserializeUint8Array, } from "@kynesyslabs/demosdk/utils" const bytes = new Uint8Array([1, 2, 3, 4]) const b64 = serializeUint8Array(bytes) const back = deserializeUint8Array(b64) // same bytes ``` `serializeUint8Array(u8)` returns a Base64 string. `deserializeUint8Array(base64)` returns the original `Uint8Array`. Useful when carrying binary payloads through JSON (e.g. wire formats that require string fields). ## Data manipulation The `dataManipulation` namespace bundles object-to-hex and forge-buffer helpers used by the wire serializers: ```typescript theme={null} import { dataManipulation } from "@kynesyslabs/demosdk/utils" const hex = await dataManipulation.ObjectToHex({ foo: "bar" }) const obj = await dataManipulation.HexToObject(hex) const forgeHex = dataManipulation.ForgeToHex(forgeBuffer) // "0x..." const forgeBuf = dataManipulation.HexToForge(forgeHex) // Uint8Array(64) const cloned = dataManipulation.copyCreate(obj) // deep clone via JSON round-trip ``` | Helper | Purpose | | ------------------ | ------------------------------------------------------------------------------------------------------------------- | | `ObjectToHex(obj)` | JSON-stringify and hex-encode an object. Async (returns `Promise`). | | `HexToObject(hex)` | Inverse of `ObjectToHex`. Async. | | `ForgeToHex(buf)` | Convert a node-forge buffer to `0x`-prefixed hex. | | `HexToForge(hex)` | Convert a 64-byte hex string to a `Uint8Array(64)` accepted by node-forge. | | `copyCreate(obj)` | Deep-clone via `JSON.parse(JSON.stringify(...))`. Loses functions / dates / undefined; intended for plain payloads. | Prefer the WebSDK-level helpers (`Demos.signMessage`, `denomination.demToOs`, etc.) for everyday use. Reach for `utils` only when you need raw byte/hex manipulation that the higher-level surface doesn't cover. # Dahr Api Reference Source: https://docs.kynesys.xyz/sdk/web2/dahr-api-reference/overview The DAHR (Data Agnostic HTTPS Relay) class handles proxied HTTP requests with attestation. Each instance maintains its own session and provides attest... # DAHR API Reference ### DAHR Class The DAHR (Data Agnostic HTTPS Relay) class handles proxied HTTP requests with attestation. Each instance maintains its own session and provides attestation for requests made through the Demos network. #### **Methods** **createDahr()** Creates a new DAHR instance: ```typescript theme={null} const dahr = await demos.web2.createDahr(): Promise ``` **startProxy()** Each `startProxy` call creates one on-chain web2Request transaction that stores only hashes of the response (body and headers), not the full data. After the transaction is confirmed and broadcast, the SDK returns its `txHash`. Makes a proxied request: ```typescript theme={null} const params: IStartProxyParams { url: string method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH" options?: { headers?: OutgoingHttpHeaders payload?: unknown authorization?: string } } await dahr.startProxy(params) ``` Returns All proxy requests return an `IWeb2Result`: ```typescript theme={null} export interface IWeb2Result { status: number statusText: string headers: IncomingHttpHeaders data: any responseHash: string // SHA-256 of response body responseHeadersHash: string // SHA-256 of response headers requestHash?: string txHash?: string } ``` # Types Source: https://docs.kynesys.xyz/sdk/web2/dahr-api-reference/types The Web2 proxy supports standard HTTP methods through the `Web2Method` type: # Types ### HTTP Methods The Web2 proxy supports standard HTTP methods through the `Web2Method` type: ```typescript theme={null} type Web2Method = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" ``` ### Request Parameters When making requests through the proxy, use the `IStartProxyParams` interface to structure your request: ```typescript theme={null} interface IStartProxyParams { url: string // Target URL method: Web2Method // HTTP method to use options?: { // Optional parameters headers?: OutgoingHttpHeaders // Request headers (string | string[]) payload?: any // Request body (POST/PUT/PATCH) authorization?: string // Bearer token (if required) } } ``` ### Response Types `txHash` is added by the SDK after confirm/broadcast; node responses may omit it. Only hashes (`responseHash`, `responseHeadersHash`) are stored on-chain; not the full response. Response type from `startProxy`: ```typescript theme={null} interface IWeb2Result { status: number // HTTP status code statusText: string // Status message headers: IncomingHttpHeaders // Response headers (string | string[]) data: any // Response data responseHash: string // SHA-256 of response body responseHeadersHash: string // SHA-256 of response headers requestHash?: string // Optional SHA-256 of request body txHash?: string // Optional On-chain tx hash (SDK attaches after broadcast) } ``` ### Usage Examples #### Handling Responses ```typescript theme={null} const response = await dahr.startProxy({ url: "https://postman-echo.com/get?test=1", method: "GET", options: { headers: { Accept: ["application/json", "text/plain"] }, authorization: "my-token-if-needed", }, }) // HTTP response + hashes + tx hash (returned by SDK) console.log(res.status, res.statusText) console.log(res.responseHash, res.responseHeadersHash) console.log(res.txHash) // can be verified on-chain // Verify on-chain (optional) if (res.txHash) { const tx = await demos.getTxByHash(res.txHash) console.log(!!tx ? "On-chain tx found" : "Tx not found") } ``` # Discord Source: https://docs.kynesys.xyz/sdk/web2/identities/discord You can associate your Discord account with your DEMOS address using message-based verification. # Discord You can associate your Discord account with your DEMOS address by posting a cryptographic proof in a Discord channel where the DEMOS bot has access. The Key Server provides an OAuth-based Discord verification flow ([`KeyServerClient.verifyOAuth("discord", ...)`](/sdk/web2/keyserver-oauth)) that returns a signed attestation. Submitting that attestation as an on-chain identity is not currently exposed by the SDK — the previous `addDiscordIdentityFromOAuth` method was reverted (SDK commit `67c5bc5`). For now, use the message-based flow below. ### 1. Connect your wallet ```typescript theme={null} import { Demos } from "@kynesyslabs/demosdk/websdk" import { Identities } from "@kynesyslabs/demosdk/abstraction"; // the DEMOS rpc const rpc = "https://node2.demos.sh" const demos = new Demos(); await demos.connect(rpc); await demos.connectWallet(mnemonic); ``` ### 2. Generate the Proof Payload To link your Discord account to your DEMOS address, you need to post a message containing a proof in a Discord channel. The proof is a string that contains a message, a signature and your public key. You can generate a proof as shown below: ```typescript theme={null} const identities = new Identities(); const proofPayload = await identities.createWeb2ProofPayload(demos) console.log("Post this in Discord:", proofPayload) ``` The proof looks like this: ``` demos:dw2p:ed25519:e9dd684a031e142ce312b695275b226ab8824f2fd3674db52f28be6c3e9fe027f88a8a9509563.... ``` Post this proof as a message in a Discord server where the DEMOS verification bot has access. The Discord message must be publicly accessible by the DEMOS network bot. Make sure you post in a channel where the bot can read messages. ### 3. Send an Identity request After posting the proof message, copy the Discord message URL and use it to create an identity transaction as shown below: To get the message URL in Discord: 1. Right-click on your message 2. Select "Copy Message Link" The URL format should look like: `https://discord.com/channels/{guildId}/{channelId}/{messageId}` ```typescript theme={null} const proof = "https://discord.com/channels/123456789/987654321/111222333444" const validityData = await identities.addDiscordIdentity(demos, proof) console.log("validity data: ", validityData) if (validityData.result == 200){ const res = await demos.broadcast(validityData) console.log(res) } ``` A successful transaction response should look like this: ```json theme={null} { "result": 200, "response": { "message": "Verified discord proof. Transaction applied." }, "require_reply": false, "extra": { "confirmationBlock": 15 } } ``` *** ## Getting linked web2 accounts After the confirmation block has been forged, you can retrieve all your linked accounts as shown: ```typescript theme={null} const web2Ids = await identities.getWeb2Identities(demos) ``` The response should look like this: ```json theme={null} { "result": 200, "response": { "discord": [ { "proof": "https://discord.com/channels/123456789/987654321/111222333444", "username": "your_discord_username", "proofHash": "a1b2c3d4e5f6..." } ] }, "require_reply": false, "extra": null } ``` ## Removing Discord identity You can create a transaction to remove your linked Discord account as shown: ```typescript theme={null} const payload = { context: "discord", username: "your_discord_username" } const validityData = await identities.removeWeb2Identity(demos, payload); if (validityData.result == 200) { const res = await demos.broadcast(validityData); console.log(res); } ``` A successful transaction response should look like this: ```json theme={null} { "result": 200, "response": { "message": "Identity removed. Transaction applied." }, "require_reply": false, "extra": { "confirmationBlock": 20 } } ``` ## Supported URL Formats The following Discord URL formats are supported for message-based verification: 1. Standard message URL: `https://discord.com/channels/{guildId}/{channelId}/{messageId}` 2. PTB (Public Test Build): `https://ptb.discord.com/channels/{guildId}/{channelId}/{messageId}` # Ethos Source: https://docs.kynesys.xyz/sdk/web2/identities/ethos Link an Ethos reputation score to your DEMOS address. Ethos provides peer-reviewed credibility scores for EVM wallets. # Ethos You can associate an Ethos-scored EVM wallet with your DEMOS address by following these steps. ### 1. Connect your wallet ```typescript theme={null} import { Demos } from "@kynesyslabs/demosdk/websdk" import { Identities } from "@kynesyslabs/demosdk/abstraction"; // the DEMOS rpc const rpc = "https://node2.demos.sh" const demos = new Demos(); await demos.connect(rpc); await demos.connectWallet(mnemonic); ``` ### 2. Fetch the Ethos Score Retrieve the Ethos score for the EVM wallet you want to link. The score is fetched from the node via the `gcr_routine` mechanism. The `walletAddress` argument must be a valid EVM address (`0x[a-fA-F0-9]{40}`); the SDK throws if the format is invalid. ```typescript theme={null} const identities = new Identities(); const walletAddress = "0x..." // EVM wallet to score const scoreResponse = await identities.getEthosScore(demos, walletAddress) console.log("ethos score: ", scoreResponse) ``` ### 3. Send an Identity request Build an `EthosWalletIdentity` payload and submit the link transaction. The payload is validated client-side before being sent: * `chain` — required non-empty string (e.g. `"evm"`). * `subchain` — required non-empty string (e.g. `"ethereum"`, `"base"`). * `address` — required EVM address matching `0x[a-fA-F0-9]{40}`. * `score` — required, must be a finite, non-negative number. * `lastSyncedAt` — required ISO date string (must parse with `Date.parse`). * `profileId` — optional Ethos profile id (number, if provided). * `metadata` — optional object (e.g. `displayName`, `username`). ```typescript theme={null} const payload = { chain: "evm", subchain: "ethereum", address: "0x...", score: 1450, lastSyncedAt: new Date().toISOString(), // optional profileId: 12345, metadata: { displayName: "your display name", username: "your-username", }, } const validityData = await identities.addEthosIdentity(demos, payload) console.log("validity data: ", validityData) if (validityData.result == 200) { const res = await demos.broadcast(validityData) console.log(res) } ``` A successful transaction response should look like this: ```json theme={null} { "result": 200, "response": { "message": "Ethos identity linked. Transaction applied." }, "require_reply": false, "extra": { "confirmationBlock": 14 } } ``` ### 4. Getting linked Ethos identities After the confirmation block has been forged, you can retrieve the Ethos identities linked to an address. Pass an `address` to query a specific account or omit it to use the connected wallet's address. ```typescript theme={null} const ethosIds = await identities.getEthosIdentities(demos) console.log(ethosIds) ``` ### 5. Removing an Ethos identity You can create a transaction to remove a linked Ethos wallet. The removal payload only needs the identifying fields: * `chain` — required non-empty string. * `subchain` — required non-empty string. * `address` — required EVM address (`0x[a-fA-F0-9]{40}`). ```typescript theme={null} const removePayload = { chain: "evm", subchain: "ethereum", address: "0x...", } const validityData = await identities.removeEthosIdentity(demos, removePayload) if (validityData.result == 200) { const res = await demos.broadcast(validityData) console.log(res) } ``` A successful transaction response should look like this: ```json theme={null} { "result": 200, "response": { "message": "Ethos identity removed. Transaction applied." }, "require_reply": false, "extra": { "confirmationBlock": 26 } } ``` # Github Source: https://docs.kynesys.xyz/sdk/web2/identities/github You can associate your GitHub account with your DEMOS address using gist-based verification. # GitHub You can associate your GitHub account with your DEMOS address by creating a public gist containing a cryptographic proof signed by your DEMOS wallet. The Key Server provides an OAuth-based GitHub verification flow ([`KeyServerClient.verifyOAuth("github", ...)`](/sdk/web2/keyserver-oauth)) that returns a signed attestation. Submitting that attestation as an on-chain identity is not currently exposed by the SDK — the previous `addGithubIdentityFromOAuth` method was reverted (SDK commit `67c5bc5`). For now, use the gist-based flow below. ### 1. Connect your wallet ```typescript theme={null} import { Demos } from "@kynesyslabs/demosdk/websdk"; import { Identities } from "@kynesyslabs/demosdk/abstraction"; // the DEMOS rpc const rpc = "https://node2.demos.sh" const demos = new Demos(); await demos.connect(rpc); await demos.connectWallet(mnemonic); ``` ### 2. Generate the proof payload To link your GitHub account to your DEMOS address, you need to create a public gist containing a proof on your GitHub. The proof is a string that contains a message, a signature and your public key. You can generate a proof as shown below: ```typescript theme={null} const identities = new Identities(); const proofPayload = await identities.createWeb2ProofPayload(demos) console.log("Upload on GitHub gist: ", proofPayload) ``` The proof looks like this: ``` demos:dw2p:ed25519:e9dd684a031e142ce312b695275b226ab8824f2fd3674db52f28be6c3e9fe027f88a8a/... ``` Open your GitHub and create a new public gist containing only the proof in the first file. ### 3. Send an identity request After creating the gist, copy the gist url and use it to create an identity transaction as shown below: ```typescript theme={null} const proof = "https://gist.github.com/cwilvx/abf8db960c16dfc7f6dc1da840852f79" const validityData = await identities.addGithubIdentity(demos, proof) console.log("validity data: ", validityData) if (validityData.result == 200){ const res = await demos.broadcast(validityData) console.log(res) } ``` The following proof url formats are also supported: 1. Raw gist url: [https://gist.githubusercontent.com/cwilvx/abf8db960c16dfc7f6dc1da840852f79/raw/224478424c5e6e51f5eb60cb6aeea278d3418742/gistfile1.txt](https://gist.githubusercontent.com/cwilvx/abf8db960c16dfc7f6dc1da840852f79/raw/224478424c5e6e51f5eb60cb6aeea278d3418742/gistfile1.txt) 2. Raw proof file in a GitHub repo url: [https://raw.githubusercontent.com/cwilvx/vonage-draft-images/refs/heads/master/proof.txt](https://raw.githubusercontent.com/cwilvx/vonage-draft-images/refs/heads/master/proof.txt) A successful transaction response should look like this: ```json theme={null} { "result": 200, "response": { "message": "Verified github proof. Transaction applied." }, "require_reply": false, "extra": { "confirmationBlock": 38 } } ``` *** ## Getting linked web2 accounts After the confirmation block has been forged, you can retrieve all your linked accounts as shown: ```typescript theme={null} const web2Ids = await identities.getWeb2Identities(demos) ``` The response should look like this: ```json theme={null} { "result": 200, "response": { "github": [ { "proof": "https://gist.github.com/cwilvx/abf8db960c16dfc7f6dc1da840852f79", "username": "cwilvx", "proofHash": "c9bcf4dcb2bdb490...1060ab12b16a7385351" } ] }, "require_reply": false, "extra": null } ``` ## Removing linked GitHub identity You can create a transaction to remove your linked GitHub account as shown: ```typescript theme={null} const payload = { context: "github", username: "cwilvx" } const validityData = await identities.removeWeb2Identity(demos, payload); if (validityData.result == 200) { const res = await demos.broadcast(validityData); console.log(res); } ``` A successful transaction response should look like this: ```json theme={null} { "result": 200, "response": { "message": "Identity removed. Transaction applied." }, "require_reply": false, "extra": { "confirmationBlock": 47 } } ``` # Human Passport Source: https://docs.kynesys.xyz/sdk/web2/identities/human-passport Prove humanity for your DEMOS address using Human Passport (formerly Gitcoin Passport). Verifies you are a unique human via on-chain stamps and off-chain attestations. # Human Passport You can attach a Human Passport (formerly Gitcoin Passport) Proof of Personhood score to your DEMOS address by following these steps. The default score threshold the node enforces when linking is **20**. ### 1. Connect your wallet ```typescript theme={null} import { Demos } from "@kynesyslabs/demosdk/websdk" import { Identities } from "@kynesyslabs/demosdk/abstraction"; // the DEMOS rpc const rpc = "https://node2.demos.sh" const demos = new Demos(); await demos.connect(rpc); await demos.connectWallet(mnemonic); ``` ### 2. Fetch the Human Passport Score Retrieve the Human Passport score for the EVM address you want to link. The score is fetched from the node's cached data (not directly from the Human Passport API) via the `gcr_routine` mechanism. ```typescript theme={null} const identities = new Identities(); const address = "0x..." // EVM address with a Human Passport const scoreResponse = await identities.getHumanPassportScore(demos, address) console.log("human passport score: ", scoreResponse) ``` ### 3. Send an Identity request Build a `HumanPassportIdentityData` payload and submit the link transaction. Required and optional fields: * `address` — required EVM address being verified. * `verificationMethod` — required, either `"api"` or `"onchain"`. * `chainId` — required when `verificationMethod` is `"onchain"`. The SDK throws if it is missing. * `signature` — optional, for enhanced verification. * `referralCode` — optional. The node verifies the Human Passport score and persists the identity if the score meets the threshold (default: 20). ```typescript theme={null} // API verification const payload = { address: "0x...", verificationMethod: "api" as const, } // or, on-chain verification (chainId required) const payloadOnchain = { address: "0x...", verificationMethod: "onchain" as const, chainId: 10, // e.g., Optimism } const validityData = await identities.addHumanPassportIdentity(demos, payload) console.log("validity data: ", validityData) if (validityData.result == 200) { const res = await demos.broadcast(validityData) console.log(res) } ``` A successful transaction response should look like this: ```json theme={null} { "result": 200, "response": { "message": "Human Passport identity linked. Transaction applied." }, "require_reply": false, "extra": { "confirmationBlock": 11 } } ``` ### 4. Getting linked Human Passport identities After the confirmation block has been forged, you can list the Human Passport identities linked to a DEMOS address. The method returns an array of `SavedHumanPassportIdentity` records (address, score, stamps, verification method, timestamps). ```typescript theme={null} const demosAddress = await demos.getEd25519Address() const passportIds = await identities.getHumanPassportIdentities( demos, demosAddress, ) console.log(passportIds) ``` ### 5. Removing a Human Passport identity You can create a transaction to remove a linked Human Passport identity. Only the EVM address is needed. ```typescript theme={null} const validityData = await identities.removeHumanPassportIdentity( demos, "0x...", ) if (validityData.result == 200) { const res = await demos.broadcast(validityData) console.log(res) } ``` A successful transaction response should look like this: ```json theme={null} { "result": 200, "response": { "message": "Human Passport identity removed. Transaction applied." }, "require_reply": false, "extra": { "confirmationBlock": 24 } } ``` # Nomis Source: https://docs.kynesys.xyz/sdk/web2/identities/nomis Link a Nomis wallet score to your DEMOS address. Nomis provides on-chain reputation scores derived from wallet activity across multiple chains. # Nomis You can associate a Nomis-scored wallet with your DEMOS address by following these steps. Nomis works with wallets across multiple chains (e.g. `"evm"`, `"solana"`); supply the optional `chain` and `subchain` parameters when fetching the score to target a specific network. ### 1. Connect your wallet ```typescript theme={null} import { Demos } from "@kynesyslabs/demosdk/websdk" import { Identities } from "@kynesyslabs/demosdk/abstraction"; // the DEMOS rpc const rpc = "https://node2.demos.sh" const demos = new Demos(); await demos.connect(rpc); await demos.connectWallet(mnemonic); ``` ### 2. Fetch the Nomis Score Retrieve the Nomis score for the wallet you want to link. The score is fetched from the node's cached data via the `gcr_routine` mechanism. ```typescript theme={null} const identities = new Identities(); const walletAddress = "0x..." // wallet to score const chain = "evm" // optional, e.g. "evm" or "solana" const subchain = "ethereum" // optional subchain / network identifier const scoreType = 1 // optional Nomis score type identifier const scoreResponse = await identities.getNomisScore( demos, walletAddress, chain, subchain, scoreType, ) console.log("nomis score: ", scoreResponse) ``` The `chain`, `subchain`, and `scoreType` parameters are all optional; omit them to use Nomis defaults. ### 3. Send an Identity request Build a `NomisWalletIdentity` payload using the score data and submit the link transaction. The minimum required fields are `chain`, `subchain`, `address`, `score`, `scoreType`, and `lastSyncedAt`. ```typescript theme={null} const payload = { chain: "evm", subchain: "ethereum", address: walletAddress, score: 750, scoreType: 1, lastSyncedAt: new Date().toISOString(), // optional mintedScore: null, metadata: { referralCode: "your-referral-code", }, } const validityData = await identities.addNomisIdentity(demos, payload) console.log("validity data: ", validityData) if (validityData.result == 200) { const res = await demos.broadcast(validityData) console.log(res) } ``` A successful transaction response should look like this: ```json theme={null} { "result": 200, "response": { "message": "Nomis identity linked. Transaction applied." }, "require_reply": false, "extra": { "confirmationBlock": 12 } } ``` ### 4. Getting linked Nomis identities Linked Nomis identities are exposed through the standard identity bundle on the DEMOS account. After the confirmation block has been forged, query the account's linked identities through the RPC to confirm the link. ### 5. Removing a Nomis identity You can create a transaction to remove a linked Nomis wallet. The same `NomisWalletIdentity` payload shape is used to identify which entry to remove. ```typescript theme={null} const validityData = await identities.removeNomisIdentity(demos, payload) if (validityData.result == 200) { const res = await demos.broadcast(validityData) console.log(res) } ``` A successful transaction response should look like this: ```json theme={null} { "result": 200, "response": { "message": "Nomis identity removed. Transaction applied." }, "require_reply": false, "extra": { "confirmationBlock": 25 } } ``` # Identities Source: https://docs.kynesys.xyz/sdk/web2/identities/overview You can associate your web2 account to your DEMOS using the DEMOS sdk. # Identities You can associate your web2 account, on-chain reputation, or domain ownership to your DEMOS address using the DEMOS sdk. The following providers are supported: **Web2 accounts** 1. [Twitter](/sdk/web2/identities/twitter) 2. [Github](/sdk/web2/identities/github) 3. [Discord](/sdk/web2/identities/discord) 4. [Telegram](/sdk/web2/identities/telegram) **Reputation & Proof of Personhood** 5. [Nomis](/sdk/web2/identities/nomis) — multi-chain wallet reputation score. 6. [Ethos](/sdk/web2/identities/ethos) — peer-reviewed credibility score for EVM wallets. 7. [Human Passport](/sdk/web2/identities/human-passport) — Proof of Personhood (formerly Gitcoin Passport). **Domains** 8. [Unstoppable Domains](/sdk/web2/identities/unstoppable-domains) — link a UD domain (EVM or Solana signing flow). # Telegram Source: https://docs.kynesys.xyz/sdk/web2/identities/telegram You can associate your Telegram account with your DEMOS address by following these steps. This page provides two integration styles: # Telegram ## Telegram Identity Integration (GitBook Page) You can associate your Telegram account with your DEMOS address by following these steps. This page provides two integration styles: 1. High-level (SDK + helper service approach) 2. Low-level (explicit challenge + bot attestation flow) The Telegram flow differs from GitHub / Twitter because it involves an authorized Telegram Bot that produces an unsigned on‑chain identity transaction after verifying both the user’s Telegram login payload and a signed wallet challenge. *** ### Telegram Bot Overview Flow: dApp → Telegram Bot (verification with commands) -> Verification on dApp. The Telegram bot (or a verification microservice) acts as an attestation authority. It must: 1. Accept a POST containing the Telegram Login Widget auth payload (`telegramUser`) and the `signedChallenge` produced by the user wallet. 2. Verify the Telegram auth payload hash (HMAC) server-side. 3. Verify the user wallet signature embedded in `signedChallenge` (delegated to node during `/api/tg-verify`). 4. Sign an attestation with the bot's Ed25519 key and call the node `/api/tg-verify` endpoint. 5. Return the resulting unsigned identity transaction to the dApp. Only the widget-based REST flow is covered here. #### Minimal Bot Environment Variables | Variable | Purpose | Example | | --------------------- | --------------------------------------- | ------------------------- | | `BOT_TOKEN` | Telegram Bot API token | `123456:ABCDEF...` | | `GENESIS_PRIVATE_KEY` | Bot Ed25519 private key (hex, 64 bytes) | `0x...` | | `GENESIS_PUBLIC_KEY` | Bot public key whitelisted in genesis | `0x...` | | `NODE_URL` | Demos node base URL | `http://127.0.0.1:53550` | | `BOT_USERNAME` | Telegram Bot Username | `yourTelegramBotUsername` | #### Attestation Signing Logic (Core `attest` Function) The bot builds an ordered payload then signs it with its private key. Ordering matters to ensure deterministic signature. ```ts theme={null} async function attest(telegramId: string, username: string, signedChallenge: string) { const payload = { telegram_id: telegramId, username, signed_challenge: signedChallenge, timestamp: Math.floor(Date.now()/1000) } // Lexicographic ordering of keys for consistent signing const ordered = Object.keys(payload).sort().reduce((a,k)=> (a[k] = (payload as any)[k], a), {} as any) const msg = JSON.stringify(ordered) const sig = nacl.sign.detached(Buffer.from(msg,'utf8'), Buffer.from(GENESIS_PRIVATE_KEY,'hex')) const bot_signature = Buffer.from(sig).toString('hex') const body = { ...payload, bot_address: GENESIS_PUBLIC_KEY, bot_signature } const r = await fetch(`${NODE_URL}/api/tg-verify`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body) }) const text = await r.text() if(!r.ok) throw new Error('node '+r.status+' '+text) const j = text ? JSON.parse(text) : {} if(!j.success) throw new Error(j.error || 'verify_failed') return j // { success:true, unsignedTransaction: {...} } } ``` Security guarantees enforced by node: * Bot key (public) must match a whitelisted genesis entry. * Challenge hash embedded in unsigned transaction prevents reuse. * User signature inside `signed_challenge` ties Telegram identity to wallet. #### Telegram Auth Payload HMAC Verification (Server-Side) Telegram instructs verifying the Login Widget payload like this: ```ts theme={null} import crypto from 'node:crypto' function verifyTelegramAuth(auth: Record, botToken: string) { const { hash, ...rest } = auth // 1. Sort keys alphabetically const dataCheckArr = Object.keys(rest) .sort() .map(k => `${k}=${rest[k]}`) const dataCheckString = dataCheckArr.join('n') // 2. Secret key = SHA256(botToken) const secretKey = crypto.createHash('sha256').update(botToken).digest() // 3. Compute HMAC-SHA256 of dataCheckString with secretKey const hmac = crypto .createHmac('sha256', secretKey) .update(dataCheckString) .digest('hex') return hmac === hash } ``` If this returns false, reject the request and DO NOT proceed to attestation. #### Verification REST Endpoint (Unified) Example ```ts theme={null} import express from 'express' import nacl from 'tweetnacl' import crypto from 'node:crypto' const app = express(); app.use(express.json()) const BOT_TOKEN = process.env.BOT_TOKEN! const GENESIS_PRIVATE_KEY = process.env.GENESIS_PRIVATE_KEY!.replace(/^0x/,'') const GENESIS_PUBLIC_KEY = process.env.GENESIS_PUBLIC_KEY! const NODE_URL = process.env.NODE_URL || 'http://127.0.0.1:53550' function (auth: any) { const { hash, ...rest } = auth const dataCheckString = Object.keys(rest).sort().map(k => `${k}=${rest[k]}`).join('n') const secretKey = crypto.createHash('sha256').update(BOT_TOKEN).digest() const hmac = crypto.createHmac('sha256', secretKey).update(dataCheckString).digest('hex') return hmac === hash } async function callVerify(telegramUser: any, signedChallenge: string) { const payload = { telegram_id: telegramUser.id, username: telegramUser.username || '', signed_challenge: signedChallenge, timestamp: Math.floor(Date.now()/1000) } const ordered = Object.keys(payload).sort().reduce((a,k)=> (a[k]=(payload as any)[k], a), {} as any) const msg = JSON.stringify(ordered) const sig = nacl.sign.detached(Buffer.from(msg,'utf8'), Buffer.from(GENESIS_PRIVATE_KEY,'hex')) const bot_signature = Buffer.from(sig).toString('hex') const body = { ...payload, bot_address: GENESIS_PUBLIC_KEY, bot_signature } const r = await fetch(`${NODE_URL}/api/tg-verify`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body) }) const text = await r.text(); if(!r.ok) throw new Error(text || r.status.toString()) const j = text ? JSON.parse(text) : {} if(!j.success) throw new Error(j.error || 'verify_failed') return j } app.listen(8787, () => console.log('Verification service listening on :8787')) ``` This single endpoint now handles HMAC validation, attestation signing, and forwarding to the node. #### Optional Proxy / Forwarder Pattern (Server Relay) If you keep the verification service private, expose a thin relay through your main backend (adapted from `examples/BotWebhookForwarder.ts`): ```ts theme={null} app.post('/api/telegram/verify', async (req, res) => { const { telegramAuth, signedChallenge } = req.body if(!telegramAuth || !signedChallenge) return res.status(400).json({ error:'Missing telegramAuth or signedChallenge' }) const resp = await fetch(`${BOT_INTERNAL_URL}/webhook/verification`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ telegramUser: telegramAuth, signedChallenge, timestamp: Math.floor(Date.now()/1000), source:'dapp-server' }) }) const data = await resp.json() if(!resp.ok) return res.status(resp.status).json({ error: data.error || 'Bot verification failed' }) res.json(data) }) ``` Benefits: * Hides internal verification service * Central HMAC verification & logging * Add auth / API keys / rate limiting easily *** ### 1. Connect your wallet Use the DEMOS SDK (web) and obtain (or generate) a wallet/key pair. In production you’d use an existing wallet provider; below we show a mnemonic example only for demonstration. ```ts theme={null} import { Demos } from "@kynesyslabs/demosdk/websdk"; // the DEMOS rpc (HTTP endpoint of a node) const rpc = "https://node2.demos.sh"; const demos = new Demos(); await demos.connect(rpc); await demos.connectWallet(mnemonic); // Replace with secure wallet handling console.log("Address:", demos.wallet.address); ``` *** ### 2. Obtain a one‑time challenge Open the telegram bot on your telegram app, and get the challenge by writing /link command. *** ### 3. Sign the challenge (prove wallet ownership) ```ts theme={null} const signature = await demos.wallet.signMessage(challenge); const signedChallenge = `${challenge}:${signature}`; console.log('Signed challenge:', signedChallenge.slice(0, 96) + '…'); ``` Never reuse a challenge. If the user abandons the flow, discard it and request a new one later. *** ### 4. Authenticate the user with the Telegram Login Widget Send the authentication payload to the telegram bot as message like this format: ``` ``` where signature will be signed challenge which gave the bot, and the public key, will be your wallet public key. ### 5. Check telegram channel membership Ensure the user is a member of a specific Telegram channel to proceed with the identity attestation. ```ts theme={null} export async function checkMembership( botWebhook: string, telegramUser: TelegramUserAuth, ): Promise { const r = await fetch( `${botWebhook.replace(//$/, "")}/webhook/check-membership`, { method: "POST", headers: { "Content-Type": "application/json", "User-Agent": "YourAppNameAndVersion/1.0", }, body: JSON.stringify({ telegramUser: telegramUser, timestamp: Date.now(), source: "your-app-name", }), }, ); if (!r.ok) throw new Error(`Challenge HTTP ${r.status}`); const j = await r.json(); if (j.error) throw new Error(j.error); return j.isMember; } const isMember = await checkMembership(botUrl, telegramUser); if (!isMember) throw new Error('User is not a channel member'); ``` This function checks if the user is a member of the specified channel using Telegram's `getChatMember` API method. If the user is not a member, an error is thrown, preventing further steps in the attestation process. *** ### 6. Send an identity attestation request to your Telegram Bot Your bot exposes an internal HTTPS endpoint (e.g. `/webhook/verification`). Send it: ```ts theme={null} async function submitAttestationRequest(botUrl: string, telegramUser: any, signedChallenge: string) { const r = await fetch(`${botUrl}/webhook/verification`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ telegramUser, signedChallenge, timestamp: Math.floor(Date.now()/1000) }) }); if(!r.ok) throw new Error('Bot HTTP ' + r.status); const j = await r.json(); if(!j.success) throw new Error(j.error || 'Bot attestation failed'); return j.unsignedTransaction; // Opaque unsigned identity tx } const unsignedTx = await submitAttestationRequest(botUrl, telegramUserPayload, signedChallenge); console.log('Unsigned identity tx:', unsignedTx); ``` Bot responsibilities: 1. Verify Telegram auth payload (hash + expiry) 2. Validate wallet signature over challenge 3. Ensure challenge not reused 4. Call node `/api/tg-verify` to obtain unsigned identity transaction 5. Return unsigned transaction to dApp *** ### 7. Sign & broadcast the identity transaction ```ts theme={null} // Sign with wallet, confirm with node, then broadcast const signedTx = await demos.wallet.signTransaction(unsignedTx); const validity = await demos.confirm(signedTx); const broadcastRes = await demos.broadcast(validity); console.log('Broadcast result:', broadcastRes); const txHash = broadcastRes.hash || broadcastRes.tx_hash; console.log('Telegram identity linked. Tx hash:', txHash); ``` Successful broadcast response (example): ```json theme={null} { "result": 200, "response": { "message": "Verified telegram proof. Transaction applied." }, "require_reply": false, "extra": { "confirmationBlock": 42 } } ``` *** ### 8. Getting linked web2 (Telegram) accounts After the confirmation block has been forged you can query all linked web2 identities (includes `telegram` array if present): ```ts theme={null} import { Identities } from "@kynesyslabs/demosdk/abstraction"; const identities = new Identities(); const web2Ids = await identities.getWeb2Identities(demos); console.log(web2Ids); ``` Example response snippet: ```json theme={null} { "result": 200, "response": { "telegram": [ { "username": "example_user", "telegramId": 123456789, "proofHash": "ab3f...c901", "challengeHash": "7ff2...19d0" } ] }, "require_reply": false, "extra": null } ``` *** ### 9. Removing linked Telegram identity Create a removal transaction using the web2 abstraction (context = `telegram`): ```ts theme={null} const payload = { context: 'telegram', username: 'example_user' // Or other unique handle stored during linking }; const validityData = await identities.removeWeb2Identity(demos, payload); if (validityData.result === 200) { const res = await demos.broadcast(validityData); console.log(res); } ``` Successful removal response: ```json theme={null} { "result": 200, "response": { "message": "Identity removed. Transaction applied." }, "require_reply": false, "extra": { "confirmationBlock": 57 } } ``` *** ### End-to-End (Headless Helper) If you prefer a single orchestrator call, adapt the following (mirrors `examples/flowFunctions.ts`): ```ts theme={null} import { runTelegramIdentityFlow } from './examples/flowFunctions'; const result = await runTelegramIdentityFlow({ nodeUrl: rpc, botUrl, wallet: demos.wallet, demos, telegramUser: telegramUserPayload, onStage: (s, d) => console.log('[stage]', s, d) }); console.log('Flow complete:', result.hash); ``` Stages emitted: * `challenge:request` → `challenge:received` → `challenge:signed` → `tx:unsigned` → `tx:broadcasted` *** ### Architecture Summary | Step | Actor | Purpose | Security Aspect | | ----------------- | ------------- | ------------------------ | ------------------------------- | | getChallenge | Node | One-time challenge | Timestamp + nonce (anti replay) | | signChallenge | Wallet | Prove address ownership | Ed25519 signature | | Telegram auth | User + Widget | Prove Telegram control | HMAC hash w/ bot token | | Attestation (bot) | Bot | Build unsigned tx | Bot key authorized in genesis | | Sign & broadcast | Wallet + Node | Record identity on-chain | Node validates all proofs | *** ### Environment Variables (Frontend Example) | Variable | Description | Example | | ------------------------------ | ------------------------------ | ------------------------------ | | `VITE_DEMOS_NODE_URL` | Demos node base URL | `https://node2.demos.sh` | | `VITE_TELEGRAM_BOT_SERVER_URL` | Your bot verification endpoint | `https://your-bot.example.com` | | `VITE_TELEGRAM_BOT_USERNAME` | Bot username (for widget) | `demos_prod_bot` | | `VITE_FRONTEND_URL` | Public origin hosting widget | `https://app.example.com` | *** ### Common Errors & Remedies | Error Pattern | Likely Cause | Suggested Handling | | ------------------------ | ---------------------------------------- | ----------------------------------------------------- | | `Challenge HTTP 4xx/5xx` | Node unreachable or malformed address | Retry / show maintenance message | | `Bot HTTP 5xx` | Bot offline / network issue | Exponential backoff retry | | `Bot attestation failed` | Invalid Telegram auth or stale challenge | Force new Telegram login + new challenge | | `BroadcastFailure` | Network congestion | Retry with fresh nonce (re-fetch challenge if needed) | | User cancels sign | Wallet rejection | Offer retry; do not reuse challenge | *** ### Security Notes * Never trust the Telegram auth payload hash solely on the client; verify server-side where possible. * Expire unused challenges (e.g. 5–10 minutes) and reject reuse server-side. * Rate limit challenge requests per address/IP. * Log stage transitions with a correlation ID (address or session) for auditing. *** ### Minimal UI Flow (Conceptual) 1. User clicks “Link Telegram” 2. UI must tell user to open the telegram bot 3. Place where user user can paste and sign the chalange from the bot 4. Show confirmation (tx hash + linked username) *** ### Complete Flow Diagram ``` User | Web App | Telegram Modal | Demos Wallet | Telegram Bot | Backend -----------------|------------------|------------------|-----------------|-----------------|----------------- Click "Start Telegram Verification" |-> Open modal |----------------->| | | Show instructions to get challenge Show instructions|<-----------------| Request challenge|-------------------------------------------------> Telegram Bot | | | |-> generateTelegramChallenge(address) | | | |-----------------> Backend | | | |<----------------- {challenge, expiresAt} Display challenge|<------------------------------------------------- Telegram Bot Paste challenge |-> Telegram Modal |----------------->|-> signMessage(challenge) | |<----------------- signature Provide signature|-> Web App |----------------->| Consume challenge|-> Backend |----------------->| Valid & unused | | | | |<-- true Refresh address |-> Web App refresh info (hasTelegramIdentity) Verification complete |<-----------------| Telegram Modal Success & auto-close |<-----------------| Telegram Modal Invalid/expired/used | | | | |<-- false Error & retry |<-----------------| Telegram Modal ``` *** ### Cleanup / Removal To unlink, use `removeWeb2Identity` with `context: 'telegram'` (see removal step). The chain state will reflect the removal in a new block. *** ### Next Steps * Integrate server-side Telegram HMAC validation * Replace mnemonic demo wallet with production wallet provider * Add UI states for: waiting bot, signing, broadcast pending, final confirmation * Implement telemetry on each stage for monitoring *** This completes the Telegram identity linking guide. # Twitter Source: https://docs.kynesys.xyz/sdk/web2/identities/twitter You can associate your Twitter account with your DEMOS address by following these steps: # Twitter You can associate your Twitter account with your DEMOS address by following these steps: ### 1. Connect your wallet ```typescript theme={null} import { Demos } from "@kynesyslabs/demosdk/websdk" import { Identities } from "@kynesyslabs/demosdk/abstraction"; // the DEMOS rpc const rpc = "https://node2.demos.sh" const demos = new Demos(); await demos.connect(rpc); await demos.connectWallet(mnemonic); ``` ### 2. Generate the Proof Payload To link your Twitter account to your DEMOS address, you need to create a public tweet containing a proof on your twitter account. The proof is a string that contains a message, a signature and your public key. You can generate a proof as shown below: ```typescript theme={null} const identities = new Identities(); const proofPayload = await identities.createWeb2ProofPayload(demos) console.log("Tweet this:", proofPayload) ``` The proof looks like this: ``` demos:dw2p:ed25519:e9dd684a031e142ce312b695275b226ab8824f2fd3674db52f28be6c3e9fe027f88a8a9509563.... ``` Open Twitter and create a public tweet containing only the proof string. ### 3. Send an Identity request After tweeting the proof, copy the tweet url and use it to create an identity transaction as shown below: ```typescript theme={null} const proof = "https://x.com/cwilvxi/status/1905612827840196726" const validityData = await identities.addTwitterIdentity(demos, proof) console.log("validity data: ", validityData) if (validityData.result == 200){ const res = await demos.broadcast(validityData) console.log(res) } ``` A successful transaction response should look like this: ```json theme={null} { "result": 200, "response": { "message": "Verified twitter proof. Transaction applied." }, "require_reply": false, "extra": { "confirmationBlock": 9 } } ``` ### 4. Getting linked web2 accounts After the confirmation block has been forged, you can retrieve all your linked accounts as shown: ```typescript theme={null} const web2Ids = await identities.getWeb2Identities(demos) ``` The response should look like this: ```json theme={null} { "result": 200, "response": { "twitter": [ { "proof": "https://x.com/cwilvxi/status/1905612827840196726", "username": "cwilvxi", "proofHash": "d1ebcb162c3d5c7d...a6078a90ee56fb21652" } ] }, "require_reply": false, "extra": null } ``` ### 5. Removing Twitter identity You can create a transaction to remove your linked Twitter account as shown: ```typescript theme={null} const payload = { context: "twitter", username: "cwilvxi" } const validityData = await identities.removeWeb2Identity(demos, payload); if (validityData.result == 200) { const res = await demos.broadcast(validityData); console.log(res); } ``` A successful transaction response should look like this: ```json theme={null} { "result": 200, "response": { "message": "Identity removed. Transaction applied." }, "require_reply": false, "extra": { "confirmationBlock": 25 } } ``` # Unstoppable Domains Source: https://docs.kynesys.xyz/sdk/web2/identities/unstoppable-domains Link an Unstoppable Domain (e.g., brad.crypto) to your DEMOS address. Supports EVM and Solana signing flows. # Unstoppable Domains You can associate an Unstoppable Domain (e.g. `brad.crypto`) with your DEMOS address. The SDK supports both EVM (MetaMask) and Solana (Phantom) signing flows: you sign a challenge with one of the domain's authorized addresses, and that signature is submitted to the DEMOS network for verification. The end-to-end flow: 1. **Resolve** the domain to fetch its authorized addresses and metadata. 2. **Generate a challenge** that includes your Demos public key and the chosen signing address. 3. **Sign the challenge** externally with the user's wallet (MetaMask `personal_sign` for EVM, Phantom for Solana). 4. **Submit** the signature, challenge, and resolution data to link the identity. ### 1. Connect your wallet ```typescript theme={null} import { Demos } from "@kynesyslabs/demosdk/websdk" import { Identities } from "@kynesyslabs/demosdk/abstraction"; // the DEMOS rpc const rpc = "https://node2.demos.sh" const demos = new Demos(); await demos.connect(rpc); await demos.connectWallet(mnemonic); ``` ### 2. Resolve the Domain Use `resolveUDDomain` to fetch the unified resolution result for the domain. The returned `UnifiedDomainResolution` contains the network, registry type, and the list of `authorizedAddresses` that can sign the linking challenge. ```typescript theme={null} const identities = new Identities(); const domain = "brad.crypto" const resolutionData = await identities.resolveUDDomain(demos, domain) console.log(resolutionData) // Pick a signing address from the authorized addresses returned by the resolver const signingAddress = resolutionData.authorizedAddresses[0].address ``` The signing address must either be the domain owner or appear in `resolutionData.authorizedAddresses`; otherwise `addUnstoppableDomainIdentity` will throw. ### 3. Generate a Challenge and Sign It Generate a challenge string that ties the signing address to your Demos public key, then have the user sign it with their wallet. For EVM, this is typically a MetaMask `personal_sign` call; for Solana, use Phantom. ```typescript theme={null} // Use the Demos public key (ed25519) for the challenge const demosPublicKey = await demos.getEd25519Address() // Generate the challenge string locally const challenge = identities.generateUDChallenge( demosPublicKey, signingAddress, ) // User signs the challenge externally (EVM example with MetaMask) const signature = await ethereum.request({ method: "personal_sign", params: [challenge, signingAddress], }) ``` For Solana, prompt the user to sign `challenge` with Phantom and pass the resulting signature in the next step. ### 4. Send an Identity request Submit the signing address, signature, challenge, and the resolution data returned in step 2. An optional `referralCode` may be passed as the final argument. ```typescript theme={null} const validityData = await identities.addUnstoppableDomainIdentity( demos, signingAddress, signature, challenge, resolutionData, // referralCode (optional) ) console.log("validity data: ", validityData) if (validityData.result == 200) { const res = await demos.broadcast(validityData) console.log(res) } ``` A successful transaction response should look like this: ```json theme={null} { "result": 200, "response": { "message": "Unstoppable Domain identity linked. Transaction applied." }, "require_reply": false, "extra": { "confirmationBlock": 13 } } ``` ### 5. Getting linked UD identities After the confirmation block has been forged, you can list the Unstoppable Domain identities linked to an address. Pass an `address` to query a specific account, or omit it to use the connected wallet's address. ```typescript theme={null} const udIds = await identities.getUDIdentities(demos) console.log(udIds) ``` ### 6. Removing a UD identity You can create a transaction to unlink a domain. Only the domain string is needed. ```typescript theme={null} const validityData = await identities.removeUnstoppableDomainIdentity( demos, "brad.crypto", ) if (validityData.result == 200) { const res = await demos.broadcast(validityData) console.log(res) } ``` A successful transaction response should look like this: ```json theme={null} { "result": 200, "response": { "message": "Unstoppable Domain identity removed. Transaction applied." }, "require_reply": false, "extra": { "confirmationBlock": 27 } } ``` # Key Server OAuth Source: https://docs.kynesys.xyz/sdk/web2/keyserver-oauth Verify user ownership of GitHub and Discord accounts with DAHR attestation using the KeyServerClient. # Key Server OAuth The `KeyServerClient` enables dApps to verify user ownership of GitHub and Discord accounts through the Key Server OAuth flow. Each successful verification produces a **DAHR (Demos Attestation Hash Response)** attestation - a cryptographic proof that the Key Server performed the verification. ## Installation The KeyServerClient is included in the SDK: ```bash theme={null} npm install @kynesyslabs/demosdk # or bun add @kynesyslabs/demosdk ``` ## Quick Start ```typescript theme={null} import { KeyServerClient } from "@kynesyslabs/demosdk/keyserver"; const client = new KeyServerClient({ endpoint: "http://localhost:3030", // Node RPC endpoint nodePubKey: "abc123...", // Node's Ed25519 public key }); // Complete OAuth verification const result = await client.verifyOAuth("github", { onAuthUrl: (url) => window.open(url), }); console.log("Verified user:", result.user.username); console.log("Provider ID:", result.user.providerId); ``` *** ## API Reference ### KeyServerClient #### Constructor ```typescript theme={null} const client = new KeyServerClient({ endpoint: string; // Key Server endpoint URL nodePubKey: string; // Node's Ed25519 public key (hex) defaultWalletAddress?: string; // Optional default wallet for binding }); ``` #### getProviders() Get list of available OAuth providers. ```typescript theme={null} const providers = await client.getProviders(); // Returns: ["github", "discord"] ``` #### initiateOAuth() Start an OAuth flow and get the authorization URL. ```typescript theme={null} const result = await client.initiateOAuth("github", { scopes?: string[]; // Optional custom scopes timeout?: number; // Flow timeout in ms (default: 600000) }); // Returns: // { // success: true, // authUrl: "https://github.com/login/oauth/authorize?...", // state: "abc123", // expiresAt: 1703001234567 // } ``` #### pollOAuth() Check the status of an OAuth flow. ```typescript theme={null} const result = await client.pollOAuth(state, walletBinding?); // Parameters: // - state: string - State identifier from initiateOAuth // - walletBinding?: WalletBinding - Optional wallet binding with signature // Returns: // { // success: true, // status: "pending" | "completed" | "failed" | "expired", // result?: OAuthUserInfo, // Present when completed // attestation?: DAHRAttestation, // Present when completed // error?: { code, message } // Present when failed // } ``` #### verifyOAuth() Complete OAuth flow with automatic polling (recommended). ```typescript theme={null} const result = await client.verifyOAuth("github", { onAuthUrl: (url, state) => { // Display URL to user (e.g., open popup, show QR code) window.open(url); }, onPoll: (attempt, status) => { // Optional: Show polling progress console.log(`Attempt ${attempt}: ${status}`); }, timeout: 300000, // 5 minutes (default: 600000) pollInterval: 2000, // 2 seconds (default: 2000) scopes: ["user:email"], // Optional custom scopes walletBinding: async (state) => { // Optional: Bind a wallet address to this verification const message = `demos-oauth-bind:${state}`; const signature = await wallet.signMessage(message); return { address: wallet.address, signature, signatureType: "evm" }; }, }); // Returns: // { // success: true, // user: OAuthUserInfo, // attestation: DAHRAttestation // } ``` *** ## Types ### OAuthUserInfo User identity information from OAuth provider. ```typescript theme={null} interface OAuthUserInfo { service: "github" | "discord"; providerId: string; // Provider's user ID username: string; // Provider's username email?: string; // If email scope was requested avatarUrl?: string; // User avatar URL verifiedAt: number; // Unix timestamp } ``` ### WalletBinding Wallet binding for proving ownership during OAuth verification. ```typescript theme={null} interface WalletBinding { address: string; // Wallet address to bind signature: string; // Signature proving ownership signatureType: "evm" | "solana" | "ed25519"; // Signature scheme } ``` The message to sign is: `demos-oauth-bind:{state}` where `state` is from `initiateOAuth()`. ### DAHRAttestation Cryptographic proof of verification. ```typescript theme={null} interface DAHRAttestation { requestHash: string; // SHA256 of request payload responseHash: string; // SHA256 of response payload signature: { type: "Ed25519"; data: string; // Hex-encoded signature }; metadata: { sessionId: string; timestamp: number; keyServerPubKey: string; nodePubKey: string; version: string; walletBinding?: WalletBinding; // Present if wallet was bound }; } ``` *** ## Attestation Verification You can cryptographically verify that the Key Server actually signed the attestation: ```typescript theme={null} import { KeyServerClient, verifyOAuthAttestation } from "@kynesyslabs/demosdk/keyserver"; const result = await client.verifyOAuth("github", { ... }); // Verify the attestation const verification = verifyOAuthAttestation( result, KNOWN_KEY_SERVER_PUBKEY, // Expected Key Server public key { maxAge: 3600000, // Optional: max attestation age (default: 1 hour) expectedNodePubKey: "...", // Optional: validate specific node } ); if (!verification.valid) { console.error("Invalid attestation:", verification.reason); } ``` ### Verification Options | Option | Type | Default | Description | | -------------------- | ------ | ------- | -------------------------------------------------- | | `maxAge` | number | 3600000 | Max attestation age in ms. Set to 0 to disable. | | `expectedNodePubKey` | string | - | Validate the attestation came from a specific node | *** ## Error Handling The client throws `OAuthError` for all OAuth-related errors: ```typescript theme={null} import { KeyServerClient, OAuthError } from "@kynesyslabs/demosdk/keyserver"; try { const result = await client.verifyOAuth("github", { ... }); } catch (error) { if (error instanceof OAuthError) { switch (error.code) { case "OAUTH_DENIED": console.log("User denied access"); break; case "OAUTH_EXPIRED": console.log("OAuth flow expired"); break; case "OAUTH_TIMEOUT": console.log("Polling timed out"); break; case "NETWORK_ERROR": console.log("Network error:", error.message); break; default: console.log(`Error [${error.code}]: ${error.message}`); } } } ``` ### Error Codes | Code | Description | | ----------------------- | -------------------------------- | | `OAUTH_INVALID_SERVICE` | Invalid OAuth provider specified | | `OAUTH_NOT_CONFIGURED` | OAuth not configured on server | | `OAUTH_NOT_AVAILABLE` | OAuth service unavailable | | `OAUTH_DENIED` | User denied authorization | | `OAUTH_EXPIRED` | OAuth flow expired | | `OAUTH_PROVIDER_ERROR` | Error from OAuth provider | | `OAUTH_STATE_NOT_FOUND` | Invalid or expired state | | `OAUTH_STATE_MISMATCH` | State parameter mismatch | | `OAUTH_TIMEOUT` | Polling timed out | | `NETWORK_ERROR` | Network connection error | | `INTERNAL_ERROR` | Internal server error | *** ## Wallet Binding Wallet binding allows you to cryptographically prove ownership of a blockchain wallet during OAuth verification. The wallet address and signature are included in the DAHR attestation, creating a verifiable link between the OAuth identity and the wallet. ### Why Use Wallet Binding? * **Sybil Resistance**: Link social accounts to wallets to prevent one person from claiming multiple identities * **Access Control**: Gate access based on both wallet and verified social identity * **Attestation Integrity**: The wallet binding is part of the signed attestation, tamper-proof ### How It Works 1. User initiates OAuth flow 2. dApp receives the `state` parameter in `onAuthUrl` callback 3. User signs message `demos-oauth-bind:{state}` with their wallet 4. Signature is sent with subsequent poll requests 5. Key Server verifies the signature and includes binding in attestation ### Usage You can provide wallet binding as a function that receives the state: ```typescript theme={null} const result = await client.verifyOAuth("github", { onAuthUrl: (url) => window.open(url), walletBinding: async (state) => { // Sign with user's connected wallet const message = `demos-oauth-bind:${state}`; const signature = await wallet.signMessage(message); return { address: wallet.address, signature, signatureType: "evm" // or "solana", "ed25519" }; }, }); // The attestation now includes the wallet binding console.log("Bound wallet:", result.attestation.metadata.walletBinding?.address); ``` Or provide a pre-computed binding (if state is known ahead of time): ```typescript theme={null} const result = await client.verifyOAuth("github", { onAuthUrl: (url) => window.open(url), walletBinding: { address: "0x123...", signature: "0xabc...", signatureType: "evm" }, }); ``` ### Supported Signature Types | Type | Description | Message Format | | --------- | ------------------------------------------ | -------------- | | `evm` | Ethereum/EVM `personal_sign` or `eth_sign` | UTF-8 string | | `solana` | Solana `signMessage` | UTF-8 bytes | | `ed25519` | Raw Ed25519 signature | UTF-8 bytes | *** ## Full Example Complete example with UI feedback and wallet binding: ```typescript theme={null} import { KeyServerClient, OAuthError, verifyOAuthAttestation } from "@kynesyslabs/demosdk/keyserver"; const KEY_SERVER_PUBKEY = "your_key_server_public_key_hex"; async function verifyGitHubAccount(wallet: { address: string; signMessage: (msg: string) => Promise }) { const client = new KeyServerClient({ endpoint: "http://localhost:3030", nodePubKey: "your_node_public_key_hex", }); try { // Check available providers const providers = await client.getProviders(); console.log("Available providers:", providers); // Start verification with wallet binding const result = await client.verifyOAuth("github", { onAuthUrl: (url, state) => { // Open GitHub auth in popup const popup = window.open(url, "github-oauth", "width=600,height=700"); // Store state for CSRF protection sessionStorage.setItem("oauth_state", state); }, onPoll: (attempt, status) => { // Update UI with progress document.getElementById("status").textContent = `Waiting for authorization... (${status})`; }, timeout: 300000, // 5 minutes // Bind wallet to this OAuth verification walletBinding: async (state) => { const message = `demos-oauth-bind:${state}`; const signature = await wallet.signMessage(message); return { address: wallet.address, signature, signatureType: "evm" }; }, }); // Verify attestation cryptographically const verification = verifyOAuthAttestation(result, KEY_SERVER_PUBKEY); if (!verification.valid) { throw new Error(`Attestation invalid: ${verification.reason}`); } // Success! console.log("Verified user:", result.user.username); console.log("Provider ID:", result.user.providerId); console.log("Attestation valid:", verification.valid); // Check wallet binding in attestation if (result.attestation.metadata.walletBinding) { console.log("Bound wallet:", result.attestation.metadata.walletBinding.address); } return result; } catch (error) { if (error instanceof OAuthError) { if (error.code === "OAUTH_DENIED") { alert("You denied access. Please try again."); } else if (error.code === "OAUTH_TIMEOUT") { alert("Verification timed out. Please try again."); } else { alert(`Verification failed: ${error.message}`); } } throw error; } } ``` *** ## Architecture The Key Server OAuth flow works as follows: ``` ┌─────────┐ ┌──────────┐ ┌────────────┐ ┌──────────────┐ │ dApp │────▶│ SDK │────▶│ Node RPC │────▶│ Key Server │ │ │ │ (Client) │ │ │ │ │ └─────────┘ └──────────┘ └────────────┘ └──────────────┘ │ ▼ ┌──────────────┐ │ GitHub / │ │ Discord │ └──────────────┘ ``` 1. **dApp** calls `KeyServerClient.verifyOAuth()` 2. **SDK** sends request to **Node RPC** 3. **Node** proxies to **Key Server** (same host) 4. **Key Server** redirects user to **GitHub/Discord** 5. User authorizes, Key Server receives callback 6. Key Server creates **DAHR attestation** 7. Attestation returned through the chain to dApp The Key Server and Node communicate on the same host, so no signatures are needed for this internal communication. The DAHR attestation provides cryptographic proof to external parties. # Making Requests Source: https://docs.kynesys.xyz/sdk/web2/making-requests How to make GET and POST requests with custom headers using the DAHR proxy. # Making Requests ### GET Requests ```typescript theme={null} const response = await dahr.startProxy({ url: "https://api.example.com", method: "GET", }) ``` ### POST Requests ```typescript theme={null} const response = await dahr.startProxy({ url: "https://api.example.com", method: "POST", options: { payload: { key: "value" }, headers: { Accept: "application/json" } } }) ``` ### Custom Headers ```typescript theme={null} const response = await dahr.startProxy({ url: "https://api.example.com", method: "GET", options: { payload: { key: "value" }, headers: { Authorization: "Bearer token", "Custom-Header": "value" } } }) ``` # Web2 Source: https://docs.kynesys.xyz/sdk/web2/overview The Web2 proxy system allows you to make HTTP requests through the Demos network. Requests are verified by a network of attestation nodes, ensuring da... # Web2 ### What is Web2 Proxy? The Web2 proxy system allows you to make HTTP requests through the Demos network. Requests are verified by a network of attestation nodes, ensuring data integrity and reliability. The system provides: * Secure proxy requests through the Demos network * Session-based request handling * Support for all standard HTTP methods * Custom header and payload support ### Basic Example ```typescript theme={null} import { Demos } from "@kynesyslabs/demosdk/websdk" // Connect to demos and set up identity const demos = new Demos() await demos.connect("http://localhost:53550") await demos.connectWallet(mnemonic) // Create and use proxy const dahr = await demos.web2.createDahr() const response = await dahr.startProxy({ url: "https://api.example.com", method: "GET", }) ``` ### Key Features * Session Management: Each proxy request is handled through a dedicated session * Method Support: GET, POST, PUT, PATCH, DELETE * Custom Headers: Add your own headers to requests * Payload Support: Send data with your requests * Authorization: Include authorization tokens when needed ### Choosing Between DAHR and TLSNotary The Demos Web2 system offers two complementary methods for web attestation: **Fast proxied requests** with hash attestation. Best for general API calls where speed matters. Response hashes are stored on-chain. **Cryptographic proofs** from TLS sessions. Best for confidential data requiring verifiable attestation with selective disclosure. | Feature | DAHR | TLSNotary | | -------- | ---------------- | ----------------------- | | Speed | \~500ms | 2-5s | | Cost | Gas only | 1+ DEM + gas | | Privacy | Hash attestation | Zero-knowledge possible | | Best for | General APIs | Confidential data | # Quick Start Source: https://docs.kynesys.xyz/sdk/web2/quick-start The Web2 proxy system enables HTTP requests through the Demos network, providing privacy and attestation capabilities through DAHR (Data Agnostic HTTP... # Quick Start The Web2 proxy system enables HTTP requests through the Demos network, providing privacy and attestation capabilities through DAHR (Data Agnostic HTTPS Relay). ### Basic Setup ```typescript theme={null} import { Demos } from "@kynesyslabs/demosdk/websdk" // Connect to demos const demos = new Demos() await demos.connect("https://node2.demos.sh") await demos.connectWallet(mnemonic) ``` ### Making Your First Request ```typescript theme={null} // Create DAHR instance const dahr = await demos.web2.createDahr() // Make request const response = await dahr.startProxy({ url: "https://api.example.com", method: "GET", }) // Do something with the response console.log("Response:", response) ``` # TLSNotary API Reference Source: https://docs.kynesys.xyz/sdk/web2/tlsnotary/api-reference API reference for TLSNotary cryptographic attestation in the Demos SDK # TLSNotary API Reference ## TLSNotaryService Class The `TLSNotaryService` class manages attestation tokens and proof storage. It provides methods for both direct execution and wallet-confirmation flows. ```typescript theme={null} import { TLSNotaryService } from '@kynesyslabs/demosdk/tlsnotary'; const service = new TLSNotaryService(demos); ``` ### Methods #### requestAttestation() Requests an attestation token for a target URL. Burns 1 DEM and returns a proxy URL. ```typescript theme={null} const { proxyUrl, tokenId, expiresAt } = await service.requestAttestation({ targetUrl: 'https://api.example.com/data' }); ``` #### requestAttestationWithConfirmation() Same as `requestAttestation()` but allows user confirmation before broadcasting. **Recommended for wallet extension apps.** ```typescript theme={null} const response = await service.requestAttestationWithConfirmation( { targetUrl: 'https://api.example.com/data' }, { onConfirm: async (details) => { // Show transaction to user for approval // details.txHash, details.amount, details.description return await showUserConfirmDialog(details); } } ); ``` #### createTLSNotary() Convenience method that handles the full setup: request token → get proxy → create TLSNotary instance → initialize WASM. ```typescript theme={null} const { tlsn, tokenId, proxyUrl } = await service.createTLSNotary({ targetUrl: 'https://api.example.com/data' }); // Ready to attest immediately! const result = await tlsn.attest({ url: 'https://api.example.com/data' }); ``` #### createTLSNotaryWithConfirmation() Same as `createTLSNotary()` but with user confirmation. **Recommended for wallet extension apps.** ```typescript theme={null} const { tlsn, tokenId } = await service.createTLSNotaryWithConfirmation( { targetUrl: 'https://api.example.com/data' }, { onConfirm: async (details) => { return await showUserConfirmDialog(details); } } ); ``` #### storeProof() Stores an attestation proof on-chain or IPFS. ```typescript theme={null} const { txHash, storageFee } = await service.storeProof( tokenId, JSON.stringify(presentation), { storage: 'onchain' } // or 'ipfs' ); ``` #### storeProofWithConfirmation() Same as `storeProof()` but with user confirmation. **Recommended for wallet extension apps.** ```typescript theme={null} const { txHash, storageFee } = await service.storeProofWithConfirmation( tokenId, JSON.stringify(presentation), { storage: 'onchain' }, { onConfirm: async (details) => { return await showUserConfirmDialog(details); } } ); ``` #### calculateStorageFee() Calculate the fee for storing a proof. ```typescript theme={null} // Returns a bigint in OS (smallest units): 6 DEM for a 5KB proof (1 base + 1 per KB). // Convert for display with denomination.osToDem(fee). const fee = service.calculateStorageFee(5); ``` ### Response Types #### AttestationTokenResponse ```typescript theme={null} interface AttestationTokenResponse { proxyUrl: string; // WebSocket proxy URL for attestation tokenId: string; // Unique token ID expiresAt: number; // Unix timestamp when token expires retriesLeft: number; // Number of retry attempts remaining txHash: string; // Transaction hash of the attestation request } ``` #### StoreProofResponse ```typescript theme={null} interface StoreProofResponse { txHash: string; // Transaction hash storageFee: bigint; // Total fee burned, as a bigint in OS (smallest units) broadcastStatus: number; // HTTP status (200 = success) broadcastMessage?: string; } ``` #### TransactionDetails (for confirmation callbacks) ```typescript theme={null} interface TransactionDetails { transaction: any; // The signed transaction object txHash: string; // Transaction hash amount: bigint; // Amount being burned, as a bigint in OS (smallest units) description: string; // Human-readable description targetUrl?: string; // For attestation requests tokenId?: string; // For store transactions } ``` ### Native Transaction Types TLSNotary uses two native transaction types: #### tlsn\_request Requests a TLSNotary attestation. Creates an access token for the proxy. ```typescript theme={null} // SDK handles this internally const tx = await demos.createTransaction({ type: "native", data: ["native", { nativeOperation: "tlsn_request", args: [targetUrl] }] }) ``` **Cost**: 1 DEM (burned as fee) #### tlsn\_store Stores the proof on-chain after notarization completes. ```typescript theme={null} // SDK handles this internally const tx = await demos.createTransaction({ type: "native", data: ["native", { nativeOperation: "tlsn_store", args: [proofId, proofData] }] }) ``` **Cost**: 1 DEM base + 1 DEM per KB (burned as storage fee) ### Cost Structure | Operation | Base Cost | Variable Cost | Notes | | -------------- | --------- | ------------- | ------------------------------- | | `tlsn_request` | 1 DEM | - | Initial request fee | | `tlsn_store` | 1 DEM | +1 DEM/KB | Storage fee based on proof size | | Gas | Standard | - | Network transaction gas | **Example costs:** * Small proof (1KB): 1 + 1 + 1 = 3 DEM + gas * Medium proof (5KB): 1 + 1 + 5 = 7 DEM + gas * Large proof (20KB): 1 + 1 + 20 = 22 DEM + gas ### Error Codes | Code | Description | Resolution | | --------------------------- | ---------------------------------- | -------------------------------------- | | `TLSN_INVALID_URL` | URL is not accessible or malformed | Verify URL format and accessibility | | `TLSN_TIMEOUT` | Notarization timed out | Retry or check target server | | `TLSN_INSUFFICIENT_BALANCE` | Not enough DEM for fees | Top up DEM balance | | `TLSN_PROOF_TOO_LARGE` | Proof exceeds maximum size | Reduce response size or use redactions | | `TLSN_TOKEN_EXPIRED` | Access token expired | Request will auto-retry | | `TLSN_PROXY_UNAVAILABLE` | No proxy available for domain | Wait and retry | ## TLSNotary Class The `TLSNotary` class performs the actual WASM-based attestation. Use it after getting a proxy URL from `TLSNotaryService`. ```typescript theme={null} import { TLSNotary } from '@kynesyslabs/demosdk/tlsnotary'; const tlsn = new TLSNotary({ notaryUrl: 'http://notary.example.com:7047', websocketProxyUrl: proxyUrl, // From TLSNotaryService }); await tlsn.initialize(); // Load WASM const result = await tlsn.attest({ url: 'https://api.example.com/data', method: 'GET', headers: { 'Accept': 'application/json' }, }); ``` ### attest() Signature ```typescript theme={null} attest( request: AttestRequest, commit?: CommitRanges, onStatus?: StatusCallback, ): Promise ``` The `commit` ranges and `onStatus` callback are passed as separate positional arguments, not as fields of the `request` object. The `request` (`AttestRequest`) fields are: | Parameter | Type | Required | Description | | ------------- | ------------------------------------------------- | -------- | ------------------------------------------ | | `url` | string | Yes | The target HTTPS URL to attest | | `method` | `"GET" \| "POST" \| "PUT" \| "DELETE" \| "PATCH"` | No | HTTP method (default: "GET") | | `headers` | `Record` | No | Custom headers to include | | `body` | `string \| object` | No | Request body for POST/PUT/PATCH requests | | `maxSentData` | number | No | Max sent data in bytes (default: 16384) | | `maxRecvData` | number | No | Max receive data in bytes (default: 16384) | ### attest() Response ```typescript theme={null} interface AttestResult { presentation: PresentationJSON; // The proof data verification: VerificationResult; // Verification result (time, serverName, sent, recv, notaryKey, verifyingKey) } ``` ### verify() Verify an attestation proof: ```typescript theme={null} const result = await tlsn.verify(presentationJSON); // result: { time, serverName, sent, recv, notaryKey, verifyingKey } ``` ## Wallet Extension Integration For production dApps, users connect via wallet extensions which handle key management. The SDK supports a 3-step flow for wallet extensions: ### The 3-Step Wallet Flow 1. **Generate Transaction**: Create the unsigned transaction 2. **Sign & Confirm**: Send to wallet extension for user approval and signing 3. **Broadcast**: Send the signed transaction to the network The `WithConfirmation` methods implement this pattern: ```typescript theme={null} // Example: Full attestation with wallet extension flow const service = new TLSNotaryService(demos); // Step 1 & 2: Generate and get user confirmation const { tlsn, tokenId } = await service.createTLSNotaryWithConfirmation( { targetUrl: 'https://api.example.com/data' }, { onConfirm: async (details) => { // In a dApp, this would show a modal to the user: // "Confirm burning 1 DEM for attestation?" // The wallet extension signs when user approves return await walletExtension.requestApproval(details.transaction); } } ); // Step 3: Perform attestation (no blockchain interaction) const result = await tlsn.attest({ url: 'https://api.example.com/data' }); // Step 1 & 2 again: Store proof with confirmation await service.storeProofWithConfirmation( tokenId, JSON.stringify(result.presentation), { storage: 'onchain' }, { onConfirm: async (details) => { // "Confirm burning 6 DEM to store proof?" return await walletExtension.requestApproval(details.transaction); } } ); ``` ### Custom Headers ```typescript theme={null} const result = await tlsn.attest({ url: "https://api.example.com/data", headers: { "Authorization": "Bearer your-token", "X-Custom-Header": "value" } }); ``` ### POST Requests ```typescript theme={null} const result = await tlsn.attest({ url: "https://api.example.com/submit", method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: "value" }) }); ``` ### Commit Ranges (Selective Disclosure) Control which parts of the request/response to include in the proof: ```typescript theme={null} const result = await tlsn.attest( { url: "https://api.example.com/data" }, { sent: [{ start: 0, end: 100 }], // Include first 100 bytes of request recv: [{ start: 0, end: 500 }], // Include first 500 bytes of response } ); ``` The `attestQuick()` method accepts an `AttestOptions` object where `commit` and `onStatus` are fields: ```typescript theme={null} const result = await tlsn.attestQuick({ url: "https://api.example.com/data", commit: { sent: [{ start: 0, end: 100 }], recv: [{ start: 0, end: 500 }], }, }); ``` Larger commit ranges result in larger proofs, which increase storage costs. Only include the data you need to prove. # Build a TLSNotary Webapp Source: https://docs.kynesys.xyz/sdk/web2/tlsnotary/build-tlsnotary-webapp Step-by-step tutorial to build a browser-based TLSNotary attestation application # Build a TLSNotary Webapp This tutorial walks you through building a complete browser-based TLSNotary attestation application from scratch. By the end, you'll have a working webapp that can: * Connect to the Demos Network * Request attestation tokens * Perform TLS attestations on HTTPS endpoints * Store cryptographic proofs on-chain ## Prerequisites * Node.js 18+ or Bun * Basic knowledge of React and TypeScript * A Demos wallet with some DEM tokens for fees ## Project Setup ### Step 1: Initialize Project ```bash theme={null} mkdir tlsnotary-app cd tlsnotary-app bun init -y ``` ### Step 2: Install Dependencies The Demos SDK (`@kynesyslabs/demosdk`) includes all TLSNotary WASM files and webpack helpers. You don't need to install `tlsn-js` separately! ```bash theme={null} bun add @kynesyslabs/demosdk react react-dom comlink bun add -d webpack webpack-cli webpack-dev-server ts-loader \ html-webpack-plugin copy-webpack-plugin typescript \ @types/react @types/react-dom \ crypto-browserify stream-browserify vm-browserify buffer process ``` ### Step 3: Create Project Structure ``` tlsnotary-app/ ├── src/ │ ├── app.tsx # Main React application │ ├── worker.ts # Web Worker for WASM │ ├── TLSNotaryClient.ts # WASM client wrapper │ └── index.html # HTML template ├── webpack.config.cjs # Webpack configuration ├── tsconfig.json # TypeScript configuration └── package.json ``` ## Configuration Files ### tsconfig.json ```json theme={null} { "compilerOptions": { "target": "ES2020", "module": "ESNext", "moduleResolution": "bundler", "jsx": "react-jsx", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "lib": ["DOM", "ES2020"] }, "include": ["src/**/*"] } ``` ### webpack.config.cjs The cross-origin isolation headers are **required** for TLSNotary WASM to work. Without them, `SharedArrayBuffer` won't be available and the attestation will fail. The SDK provides webpack helpers that automatically configure WASM file copying and required polyfills: ```javascript theme={null} const webpack = require('webpack'); const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); // Import TLSNotary webpack helpers from SDK - no need to install tlsn-js! const { getTlsnWebpackConfig, getCrossOriginHeaders, } = require('@kynesyslabs/demosdk/tlsnotary/webpack'); // Get the TLSNotary webpack configuration // Output to dist root (.) because tlsn-js expects WASM files at the web root const tlsnConfig = getTlsnWebpackConfig({ wasmOutputPath: '.', // Output to dist/ root }); module.exports = { mode: 'development', entry: { app: path.join(__dirname, 'src', 'app.tsx'), }, output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist'), clean: true, publicPath: '/', }, module: { rules: [ { test: /\.(ts|tsx)$/, exclude: /node_modules/, use: ['ts-loader'], }, { test: /\.css$/, use: ['style-loader', 'css-loader'], }, ], }, resolve: { extensions: ['.js', '.jsx', '.ts', '.tsx'], fallback: { // TLSNotary polyfills from SDK ...tlsnConfig.resolve.fallback, // Additional polyfills for this app crypto: require.resolve('crypto-browserify'), stream: require.resolve('stream-browserify'), vm: require.resolve('vm-browserify'), // Stub out unused Node.js modules constants: false, readline: false, }, }, plugins: [ // TLSNotary plugins (copies WASM files from SDK) ...tlsnConfig.plugins, // App plugins new HtmlWebpackPlugin({ template: path.join(__dirname, 'src', 'index.html'), filename: 'index.html', }), // Provide Node.js globals for browser new webpack.ProvidePlugin({ process: 'process/browser', Buffer: ['buffer', 'Buffer'], }), ], experiments: { // Enable async WASM loading ...tlsnConfig.experiments, }, devServer: { port: 3000, host: 'localhost', hot: true, // Required for SharedArrayBuffer (WASM threads) - from SDK helper headers: getCrossOriginHeaders(), }, }; ``` The `getTlsnWebpackConfig()` helper automatically: * Configures CopyWebpackPlugin to copy WASM files from the SDK * Sets up Node.js polyfills required by TLSNotary * Enables async WebAssembly experiments The `getCrossOriginHeaders()` helper returns the required COOP/COEP headers for SharedArrayBuffer. ### package.json scripts Add these scripts to your `package.json`: ```json theme={null} { "scripts": { "dev": "webpack serve --mode development", "build": "webpack --mode production" } } ``` ## Source Files ### src/index.html ```html theme={null} TLSNotary Attestation App
``` ### src/worker.ts The Web Worker loads and exposes the TLSNotary WASM module. All imports come from the SDK: ```typescript theme={null} /** * Web Worker for TLSNotary WASM * * Uses Comlink to expose the tlsn-js API to the main thread. * The WASM runs in a worker to not block the UI. * * NOTE: We import from the SDK which re-exports tlsn-js classes. * This means users don't need to install tlsn-js separately! */ import * as Comlink from 'comlink'; import { init, Prover, Presentation, NotaryServer, Transcript, } from '@kynesyslabs/demosdk/tlsnotary'; // Expose the API to the main thread Comlink.expose({ init, Prover, Presentation, NotaryServer, Transcript, }); ``` ### src/TLSNotaryClient.ts This wrapper handles the WASM initialization and provides a clean API. All types come from the SDK: ```typescript theme={null} import * as Comlink from 'comlink'; import { Transcript, type TLSNotaryConfig, type AttestResult, type AttestOptions, type VerificationResult, type ProxyRequestResponse, type StatusCallback, } from '@kynesyslabs/demosdk/tlsnotary'; import type { Prover as TProver, Presentation as TPresentation } from '@kynesyslabs/demosdk/tlsnotary'; // Create worker with Comlink const worker: any = Comlink.wrap( new Worker(new URL('./worker.ts', import.meta.url)) ); export class TLSNotaryClient { private config: TLSNotaryConfig; private initialized = false; constructor(config: TLSNotaryConfig) { this.config = { loggingLevel: 'Info', ...config, }; } async initialize(): Promise { if (this.initialized) return; await worker.init({ loggingLevel: this.config.loggingLevel }); this.initialized = true; // Expose worker globally for direct Prover access (window as any).__worker = worker; } async requestProxy(targetUrl: string): Promise { if (!this.config.rpcUrl) { throw new Error('No RPC URL configured for dynamic proxy requests.'); } const response = await fetch(this.config.rpcUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ method: 'nodeCall', params: [{ message: 'requestTLSNproxy', data: { targetUrl } }], }), }); const result = await response.json(); if (result.result !== 200) { throw new Error(`Failed to request proxy: ${result.response?.message || 'Unknown error'}`); } return result.response; } async verify(presentationJSON: any): Promise { if (!this.initialized) { throw new Error('TLSNotary not initialized. Call initialize() first.'); } const proof = await new worker.Presentation(presentationJSON.data); const verifierOutput = await proof.verify(); const transcript = new Transcript({ sent: verifierOutput.transcript?.sent || [], recv: verifierOutput.transcript?.recv || [], }); return { time: verifierOutput.connection_info.time, serverName: verifierOutput.server_name || 'Unknown', sent: transcript.sent(), recv: transcript.recv(), notaryKey: 'N/A', verifyingKey: 'N/A', }; } isInitialized(): boolean { return this.initialized; } getConfig(): TLSNotaryConfig { return { ...this.config }; } } ``` ### src/app.tsx The main React application with full TLSNotary integration: ```tsx theme={null} import React, { useState, useCallback, useEffect, useRef } from 'react'; import { createRoot } from 'react-dom/client'; import { Demos } from '@kynesyslabs/demosdk/websdk'; import { TLSNotaryService } from '@kynesyslabs/demosdk/tlsnotary/service'; import { TLSNotaryClient } from './TLSNotaryClient'; // Global instances let tlsnClient: TLSNotaryClient | null = null; let demosInstance: Demos | null = null; let tlsnService: TLSNotaryService | null = null; function App() { // State const [logs, setLogs] = useState>([]); const [wasmReady, setWasmReady] = useState(false); const [walletConnected, setWalletConnected] = useState(false); const [isLoading, setIsLoading] = useState(false); const [tokenId, setTokenId] = useState(''); const [wsProxyUrl, setWsProxyUrl] = useState(''); const [result, setResult] = useState(null); // Configuration const [notaryUrl, setNotaryUrl] = useState('http://localhost:7047'); const [rpcUrl, setRpcUrl] = useState('http://localhost:53550'); const [mnemonic, setMnemonic] = useState(''); const [targetUrl, setTargetUrl] = useState('https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd'); const [method, setMethod] = useState('GET'); const [maxRecv, setMaxRecv] = useState(4096); const logsEndRef = useRef(null); // Logger const log = useCallback((msg: string, type: string = 'info') => { const timestamp = new Date().toLocaleTimeString(); setLogs(prev => [...prev, { msg: `[${timestamp}] ${msg}`, type }]); }, []); // Auto-scroll logs useEffect(() => { logsEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [logs]); // Initialize WASM on mount useEffect(() => { const init = async () => { try { log('Initializing TLSNotary WASM...', 'info'); tlsnClient = new TLSNotaryClient({ notaryUrl, rpcUrl }); await tlsnClient.initialize(); setWasmReady(true); log('WASM initialized successfully!', 'success'); } catch (error: any) { log(`WASM init failed: ${error.message}`, 'error'); } }; init(); }, []); // Connect wallet const handleConnectWallet = useCallback(async () => { if (!mnemonic.trim()) { log('Please enter a mnemonic', 'error'); return; } setIsLoading(true); try { log('Connecting to Demos Network...', 'info'); demosInstance = new Demos(); await demosInstance.connect(rpcUrl); log('Connected to node', 'success'); // For development: direct mnemonic await demosInstance.connectWallet(mnemonic.trim()); // For production: use Wallet Extension instead // await demosInstance.connectWalletExtension(); tlsnService = new TLSNotaryService(demosInstance); setWalletConnected(true); log('Wallet connected!', 'success'); } catch (error: any) { log(`Wallet connection failed: ${error.message}`, 'error'); } finally { setIsLoading(false); } }, [mnemonic, rpcUrl, log]); // Request attestation token const handleRequestToken = useCallback(async () => { if (!tlsnService) { log('Please connect wallet first', 'error'); return; } setIsLoading(true); try { log('Requesting attestation token (burns 1 DEM)...', 'info'); const response = await tlsnService.requestAttestation({ targetUrl: targetUrl, }); setTokenId(response.tokenId); setWsProxyUrl(response.proxyUrl); log(`Token ID: ${response.tokenId}`, 'success'); log(`Proxy URL: ${response.proxyUrl}`, 'info'); log(`Expires: ${new Date(response.expiresAt).toLocaleTimeString()}`, 'info'); } catch (error: any) { log(`Token request failed: ${error.message}`, 'error'); } finally { setIsLoading(false); } }, [targetUrl, log]); // Perform attestation const handleAttest = useCallback(async () => { if (!wsProxyUrl) { log('Please request a token first', 'error'); return; } setIsLoading(true); try { log('Starting TLS attestation...', 'info'); const notarizeConfig = { notaryUrl: notaryUrl, websocketProxyUrl: wsProxyUrl, maxSentData: 16384, maxRecvData: maxRecv, url: targetUrl, method: method as 'GET' | 'POST', headers: { Accept: 'application/json' }, commit: { sent: [{ start: 0, end: 100 }], recv: [{ start: 0, end: Math.min(200, maxRecv) }], }, serverIdentity: true, }; // Use the WASM worker directly const presentationJSON = await (window as any).__worker.Prover.notarize(notarizeConfig); log('Attestation complete!', 'success'); // Verify locally log('Verifying proof...', 'info'); const verification = await tlsnClient?.verify(presentationJSON); log('Verification passed!', 'success'); setResult({ presentation: presentationJSON, verification }); } catch (error: any) { log(`Attestation failed: ${error.message}`, 'error'); } finally { setIsLoading(false); } }, [wsProxyUrl, targetUrl, notaryUrl, method, maxRecv, log]); // Full attestation (request token + attest in one click) const handleFullAttestation = useCallback(async () => { if (!tlsnService) { log('Please connect wallet first', 'error'); return; } setIsLoading(true); try { // Step 1: Request token log('Requesting attestation token...', 'info'); const tokenResponse = await tlsnService.requestAttestation({ targetUrl: targetUrl, }); setTokenId(tokenResponse.tokenId); setWsProxyUrl(tokenResponse.proxyUrl); log(`Token received: ${tokenResponse.tokenId}`, 'success'); // Step 2: Perform attestation log('Starting TLS attestation...', 'info'); const notarizeConfig = { notaryUrl: notaryUrl, websocketProxyUrl: tokenResponse.proxyUrl, maxSentData: 16384, maxRecvData: maxRecv, url: targetUrl, method: method as 'GET' | 'POST', headers: { Accept: 'application/json' }, commit: { sent: [{ start: 0, end: 100 }], recv: [{ start: 0, end: Math.min(200, maxRecv) }], }, serverIdentity: true, }; const presentationJSON = await (window as any).__worker.Prover.notarize(notarizeConfig); log('Attestation complete!', 'success'); // Step 3: Verify const verification = await tlsnClient?.verify(presentationJSON); log('Verification passed!', 'success'); setResult({ presentation: presentationJSON, verification }); } catch (error: any) { log(`Full attestation failed: ${error.message}`, 'error'); } finally { setIsLoading(false); } }, [targetUrl, notaryUrl, method, maxRecv, log]); // Store proof on-chain const handleStoreProof = useCallback(async (storageType: 'onchain' | 'ipfs') => { if (!result || !tlsnService || !tokenId) { log('Missing result, service, or token ID', 'error'); return; } setIsLoading(true); try { const proofString = typeof result.presentation === 'string' ? result.presentation : JSON.stringify(result.presentation); const proofSizeKB = Math.ceil(proofString.length / 1024); const estimatedFee = tlsnService.calculateStorageFee(proofSizeKB); log(`Storing proof (${proofSizeKB} KB, ~${estimatedFee} DEM fee)...`, 'info'); const storeResult = await tlsnService.storeProof( tokenId, proofString, { storage: storageType } ); log(`Proof stored on ${storageType}!`, 'success'); log(`Transaction: ${storeResult.txHash}`, 'info'); log(`Fee: ${storeResult.storageFee} DEM`, 'info'); } catch (error: any) { log(`Store failed: ${error.message}`, 'error'); } finally { setIsLoading(false); } }, [result, tokenId, log]); return (

TLSNotary Attestation App

{/* Configuration */}

Configuration

setNotaryUrl(e.target.value)} style={{ width: '100%' }} />
setRpcUrl(e.target.value)} style={{ width: '100%' }} />

WASM Status: {wasmReady ? '✅ Ready' : '⏳ Loading...'}

{/* Wallet Connection */}

Wallet Connection

For production apps, use the Demos Wallet Extension instead of entering a mnemonic directly.