# Build with AI (/docs/ai) These docs are written to be read by coding agents as well as people. | | | | --------------------------------------------------------------------------------- | -------------------------------------------------------------------- | | [`/llms.txt`](/llms.txt) | An index of every page, one line each | | [`/llms-full.txt`](/llms-full.txt) | Every page's text in one file: paste it into your agent's context | | Any page as Markdown | The **Copy Markdown** action on each page | | [`context7.json`](https://github.com/logos-kit/logos-kit/blob/main/context7.json) | Lets [Context7](https://context7.com) index the repo for MCP clients | | [`AGENTS.md`](https://github.com/logos-kit/logos-kit/blob/main/AGENTS.md) | The repo's rules for agents: security, the QML sandbox, naming | A good first prompt: ```text Read https://logos-kit-docs.vercel.app/llms-full.txt. Then build a Basecamp ui_qml app from `nix flake init -t github:logos-kit/logos-kit#dapp` that connects a public account, shows its balance in LGO, and sends LGO with a receipt. Handle user rejection, 6108 timeouts (onLateResult) and outcome "unknown". ``` Things agents get wrong in QML, and should be told: * amounts are strings: no `Number()`, no `BigInt` (use `kit.sdk.parseUnits`); * native amounts are lepta (1 LGO = 10^9 lepta): show `kit.sdk.formatUnits(x, 9)`, parse input with `kit.sdk.parseUnits(text, 9)`; * no `Intl`, no regex lookbehind or named groups, `short` is reserved; * no network access, remote or `data:` images, or `Qt.openUrlExternally`; * read `kit.api` fresh; it's rebuilt when the network changes. # Changelog (/docs/changelog) ## Wallet 0.3.0, Testimonials 0.3.0, Faucet 0.3.0 [#wallet-030-testimonials-030-faucet-030] * **New design ("Ledger").** One monochrome look for the wallet, both apps and this site, taken from real wallets: Fuse's balance and confirm sheet, Family's account header, send keypad and black dark mode, Phantom's action tiles and sending ring, Glow's connect sheet. Light and dark both; colour only for status. Private is shown by a lock and a label. * **Approvals read like a sentence.** One line says what happens ("Send 1 LGO to Savings"), then the amount, the full destination and short rows. Native transfers show "Built into LEZ"; a program whose source doesn't match is labelled **Source mismatch**; a token transfer proposed by an app now shows the asset, amount and destination. * **Verified by you counts.** A program you rebuilt with `logos-kit verify-program` shows as verified on approval sheets too. ## 2026-10-06 · Wallet 0.2.0, Testimonials 0.2.0, Faucet 0.2.0 [#2026-10-06--wallet-020-testimonials-020-faucet-020] * **The official LEZ testnet is the default.** The wallet, the CLI (`--zone` defaults to `lez-testnet`) and the SDK (`chain: "lez:testnet"`) use `https://testnet.lez.logos.co`, which runs LEZ `v0.3.0`. The engine is re-pinned to that release (`db66590`). The preview network stays listed for wallets that saved it. See [Networks](/docs/concepts/networks). * **Testnet faucet:** Logos Kit's drip for the testnet, 1 LGO per request, each account once an hour. The wallet's **Get test LGO** and the SDK's `requestFunds` use it automatically. * **Testimonial program on the testnet:** `5YoH3xjhgeKt2mcJXW7c31bqDNCWWA4CRJxVdvzFvVef`, immutable, `TESTIMONIAL_PROGRAMS["lez:testnet"]`. Posts there count for LP-0021. The wallet recognises only the `v0.3.0` testimonial image, so the preview network's program shows as unrecognised. * **LGO units.** The native token is LOGOS (LGO), 1 LGO = 10^9 lepta. The wallet, the apps and the CLI show LGO with up to 9 decimals, nothing rounded. The SDK and the CLI's `--json` stay in lepta; `send --amount` also takes LGO (`2.5`, `2.5LGO`, `3 LGO`). * **Slower inclusion handled:** the wallet waits up to about 6 minutes before it reports a send as not included. ## 2026-09-30 · Wallet 0.1.3, Testimonials 0.1.1, Faucet 0.1.1, npm 0.1.0 [#2026-09-30--wallet-013-testimonials-011-faucet-011-npm-010] * **On npm:** [`@logos-kit/client`](https://www.npmjs.com/package/@logos-kit/client), [`@logos-kit/codec`](https://www.npmjs.com/package/@logos-kit/codec), [`@logos-kit/protocol`](https://www.npmjs.com/package/@logos-kit/protocol) and [`@logos-kit/theme`](https://www.npmjs.com/package/@logos-kit/theme) 0.1.0. Future releases publish from GitHub Actions through npm trusted publishing, with provenance. * **Redesigned wallet and apps.** A new component kit ported from 21st.dev components: approval sheets lead with the asset, amount and full destination; proving shows a step timeline; toasts, sheets, skeleton loaders, an animated balance, a copy-to-check address chip; keyboard focus on every control and stronger contrast in both themes. * **Token create confirms as `success`** from the holder's balance. * **Testimonial evidence** counts each author's own transactions before their post (prior activity), read from the blocks. * **Preview faucet:** restart-safe ledger (a request key is never paid twice), per-visitor limits, `Retry-After`. * **Docs rewritten** with a new structure, guides per task, a CLI reference and current screenshots. ## 2026-09-28 · Wallet 0.1.2, Testimonials 0.1.0, Faucet 0.1.0 [#2026-09-28--wallet-012-testimonials-010-faucet-010] * **Preview network.** The wallet, CLI and SDK default to `lez:preview`, Logos Kit's public LEZ 0.3 network, with a built-in faucet. The official testnet still runs 0.2. * **New apps in the catalog:** Logos Kit Testimonials and Logos Kit Faucet. * **Approval sheets show the requesting app's icon and name,** with the module name Basecamp attests. * **Testimonial posts re-check their stats page at signing,** so a page that filled during approval no longer costs a failed transaction. * **Honest outcomes everywhere:** public token sends now confirm as `success` from the sender's token balance; the apps show Done only on `success`, with a "not confirmed yet" state for `unknown`. * **SDK:** `lateResult` for approvals slower than 45 s; `lez_chainId` so apps follow the wallet's network; `lez_openExplorer` for sandboxed explorer links (connected apps only, rate-limited); private faucet claims report the public account that was paid (`fundedAccount`). * **Conformance kit:** a scenario-driven fake wallet and `just conformance `. * **CLI:** the wallet folder flag is `--home` (`logos-kit call` works again). ## 2026-09-27 · Wallet 0.1.1 [#2026-09-27--wallet-011] * The wallet's real app icon (0.1.0 shipped the template placeholder). ## 2026-09-27 · Wallet 0.1.0 [#2026-09-27--wallet-010] * First catalog release: the Logos Kit core module and wallet app, signed, for macOS arm64 and Linux x86-64 and arm64. * Public and private accounts, the native token and tokens, local proving, decoded approvals with source verification, grants, auto-lock, backups. # Accounts (/docs/concepts/accounts) A Logos Kit wallet holds any number of accounts on a zone, all derived from one recovery phrase. There are two kinds. | | Public | Private | | ------------------- | --------------------------------------------- | ------------------------------------------------------------ | | Balance and history | Visible to everyone on chain | Only the owner (and the wallet) can see them | | Sending | Signed and submitted by the wallet in seconds | **Proved** on the user's machine first: minutes, not seconds | | Id an app sees | The account id (base58) | An opaque per-app handle, `pvt_…`, never the account itself | | Good for | Posting testimonials, program authors, fees | Holding and moving value privately | ## Moving between them [#moving-between-them] Transfers pick their route from the two accounts: | From → to | Route | Proof | | --------------------- | ---------------- | ----- | | public → public | public transfer | no | | public → your private | **shield** | yes | | your private → public | **deshield** | yes | | private → private | private transfer | yes | Paying **someone else's** private account needs their public keys (a nullifier key and a viewing key). The wallet can export them as a small keys file that holds no secret: `logos-kit account keys `. ## What apps see [#what-apps-see] An app only ever sees the accounts the user **chose to share** in the connect sheet. Private accounts need an extra, explicit tick; the app then receives a `pvt_…` handle that means nothing to anyone else, including other apps. ```qml kit.api.connect({ accountKinds: ["public", "private"] }).then(function (s) { s.accounts.forEach(function (a) { console.log(a.kind, a.address) // "public" 9Qf… / "private" pvt_… }) }) ``` Reads (`getWalletBalance`) and proposals (`transfer`, `sendCall`) accept the handle. The wallet resolves it, checks the app's grant, and answers. ## Labels, restore and backup [#labels-restore-and-backup] * **Labels** are yours: `logos-kit account label "Savings"`, or rename in the wallet. They are stored in LEZ's own label map, so the official CLI sees them too. * **Restore** from the recovery phrase finds your used accounts again. A "first used around" date skips older blocks and makes the first sync much faster. * **Backups** are encrypted files (`logos-kit backup export `); they need the password they were made with. See [Security model](/docs/wallet/security) for how keys are stored. # How apps talk to the wallet (/docs/concepts/basecamp) A Basecamp app reaches the wallet two ways. | | Intents | Module calls | | ------------------ | ---------------------------------------------- | --------------------------------------- | | For | Connect, send, sign, get funds | Balances, accounts, status, chain reads | | Opens | The wallet's own sheet, via the Basecamp shell | Nothing: returns data | | Identity | The shell attests the requesting app | Basecamp attests the calling module | | In `metadata.json` | Listed under `uses` | `logos_kit_wallet` under `dependencies` | ``` your app ──intent──▶ Basecamp shell ──▶ wallet chooser ──▶ Logos Kit sheet │ │ user approves └────────── module call ──────────▶ logos_kit_wallet ◀─────┘ ``` The SDK hides the difference: `kit.api.transfer(…)` opens the intent, `kit.api.getWalletBalance(…)` makes the call. Both return promises. ## Intents Logos Kit provides [#intents-logos-kit-provides] | Intent | SDK call | | -------------------------- | -------------------------------------------------------------------- | | `lez.wallet.connect` | `connect()` | | `lez.transaction.send` | `transfer()`, `sendCall()`, `sendTransaction()`, `postTestimonial()` | | `lez.message.sign` | `signMessage()` | | `lez.wallet.sign_in` | `signIn()` | | `lez.wallet.request_funds` | `requestFunds()` | If more than one wallet provides an intent, Basecamp asks the user which to use. The full list is in [Intents](/docs/reference/intents). ## The sandbox [#the-sandbox] Basecamp runs `ui_qml` apps in a sandbox. Plan for it: * no network access from QML, and no remote or `data:` images; * `Qt.openUrlExternally` is blocked (use `kit.api.openExplorer`); * only files inside your module's folder load; * Qt 6.9's JavaScript engine: no `BigInt`, no `Intl`. Amounts are strings everywhere in the SDK for that reason. ## The protocol as a package [#the-protocol-as-a-package] LWS-0's types, JSON Schema, error codes and test vectors are published, so a wallet or an app in any language can implement the same contract: npm pnpm yarn bun ```bash npm i @logos-kit/protocol ``` ```bash pnpm add @logos-kit/protocol ``` ```bash yarn add @logos-kit/protocol ``` ```bash bun add @logos-kit/protocol ``` # Networks (/docs/concepts/networks) Logos Kit is built on **LEZ 0.3**. The official testnet at `testnet.lez.logos.co` runs LEZ `v0.3.0`, and the wallet, the CLI and the SDK use it by default. | Network | Chain id | Sequencer | Faucet | | ------------------------------ | ------------- | ----------------------------------- | ------------------------ | | **Official testnet** (default) | `lez:testnet` | `https://testnet.lez.logos.co` | Logos Kit's drip (below) | | Preview (legacy) | `lez:preview` | `https://lez.84.46.247.92.sslip.io` | its own drip | | Local | `lez:local` | `http://127.0.0.1:3040` | the debug genesis key | ## The official testnet [#the-official-testnet] * Zone id `lez-testnet`, chain id `lez:testnet`. It runs LEZ `v0.3.0` (`db66590`, released 2026-09-30). Its chain was reset on 2026-10-01. * Explorer: [explorer.testnet.lez.logos.co](https://explorer.testnet.lez.logos.co). * The testimonial program is deployed there as an immutable program, `5YoH3xjhgeKt2mcJXW7c31bqDNCWWA4CRJxVdvzFvVef`, and the wallet and SDK know it (`TESTIMONIAL_PROGRAMS["lez:testnet"]`). Its image is `61b2243645ead75c1aec2987bb3aac816e3a4def4c33d38640b55a0457e111a9`, built from commit `4baed37` in the `r0.1.91.1` docker builder; its source verification status is `verified_local`. **Posts here count for LP-0021 adoption.** * Transactions can take a minute or more to be included. The wallet waits up to about 6 minutes before it reports a send as not included. ### Faucet [#faucet] `https://lez-testnet-drip.84.46.247.92.sslip.io` runs `logos-kit-drip` (`crates/logos-kit-drip`) for the testnet. The wallet's **Get test LGO**, the SDK's `requestFunds` (`lez_requestFunds`) and the Faucet app use it automatically on the testnet. * 1 LGO per request; * each account once an hour, each IP five times an hour, and 60 requests an hour in all; * a result it can't prove is `outcome_unknown`. The drip keeps a durable ledger per request key, so asking again with the same key returns that recorded outcome instead of paying again. ```bash curl -s https://lez-testnet-drip.84.46.247.92.sslip.io/fund \ -H 'content-type: application/json' \ -d '{"account":"","requestKey":"any-unique-string"}' ``` Anyone can run the same faucet for another network: `LK_DRIP_KEY= LK_DRIP_SEQUENCER= logos-kit-drip`. The deployment files are in `deploy/testnet/`. ### From a browser [#from-a-browser] The official RPC sends no CORS headers, so code in a web page can't call it directly. Logos Kit runs a relay that adds them: `https://lez-testnet.84.46.247.92.sslip.io`. Use it for reads from a web page with `@logos-kit/client`. Basecamp apps and the CLI talk to the official URL and don't need it. ## The preview network (legacy) [#the-preview-network-legacy] Before the official testnet moved to 0.3, Logos Kit ran a public LEZ 0.3 network of its own, and the wallet used it by default. It's no longer the default. It stays listed for wallets that saved it, and serves as a rehearsal network. * It runs LEZ `v0.3.0-rc1` in standalone mode. Proofs are verified for real. * Its testimonial program is `4vjENywUCfC3h85mjNGFPV7R9DqvUjV2xhMkCZbJR8XK` (`TESTIMONIAL_PROGRAMS["lez:preview"]`). Wallets from 0.2.0 recognise only the `v0.3.0` testimonial image, so they show it as an unrecognised program. **Posts there don't count for LP-0021.** * Its endpoint sends CORS headers, and its drip faucet is `https://lez-drip.84.46.247.92.sslip.io` (1 LGO per request, each account once an hour, each IP five times an hour). The deployment files are in `deploy/preview-net/`. * It may be reset. The address stays as it is: a wallet refuses a saved network whose address changed. ## Switching networks [#switching-networks] In the wallet: **Settings → Network**. From the CLI: `--zone lez-preview` or `--zone lez-local` (or `LOGOS_KIT_ZONE`); the default is `lez-testnet`. Apps built on the SDK follow the wallet's network (`lez_chainId`) and reset when it changes. # Permissions (/docs/concepts/permissions) Logos Kit's rule is simple: **apps ask, the user decides, and the wallet enforces**. The policy lives in the wallet engine, not in any UI. ## Grants [#grants] Connecting gives the app a **grant**: a set of capabilities on the accounts the user picked, for this zone. | Capability | Lets the app | | -------------- | ---------------------------------------------------------------- | | `accounts` | See the shared accounts | | `read_public` | Read public balances and data | | `read_private` | Read a shared private account's balance (needs the private tick) | | `propose_tx` | Propose transactions (each one still needs approval) | | `sign_message` | Ask for message signatures (each one still needs approval) | Grants are per app, per account and per zone. The user can revoke them in **Settings → Connected apps**. A revoked app gets `4100 Unauthorized`. ## Approvals [#approvals] A grant never lets an app spend. **Every** transaction and signature is approved in the wallet's own sheet, which shows: * the requesting app (its icon and name, and the module name Basecamp attests); * what the transaction does, decoded from the exact message being signed; * the fee cap; * the program, whether its source is verified, and whether it can still be upgraded. The password is asked again for private spends, large amounts and connects. Calls the wallet can't decode are marked, and need an explicit acknowledgement. ## What an app can never do [#what-an-app-can-never-do] * Read or export a key, or the recovery phrase. * Sign or submit anything without the user's approval in the wallet. * Learn a private account's id, or which private accounts exist. * Approve on the user's behalf: only the wallet's own UI module can approve, and Basecamp attests who is calling. # Transactions (/docs/concepts/transactions) A proposal from your app becomes a transaction only after the user approves it. From then on the wallet gives you a **handle** and you follow it. ## Lifecycle [#lifecycle] ``` awaiting_approval → building → proving → signing → submitted → included ↘ rejected · dropped · expired ``` | Lifecycle | What's happening | | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `awaiting_approval` | The approval sheet is open. It expires after 5 minutes, on lock, or on a network switch. | | `building` | The wallet reads fresh chain state and builds the exact message. | | `proving` | Private routes only: RISC Zero proves the transfer locally. `proving.phase` (`preparing`, `proving`, `submitting`, `waiting_for_block`) and `proving.progress` (0–100) update. | | `signing` | Nonces and program headers are re-checked, then the wallet signs. | | `submitted` | Sent to the sequencer; `txHash` is set. | | `included` | In a block (`block.id`). **Final** for watchers. | | `rejected` · `dropped` · `expired` | Didn't happen. `error` says why. | On the official testnet a transaction can take a minute or more to be included. The wallet waits up to about 6 minutes before it reports a send as not included. The protocol reserves `finalized`, but the wallet doesn't report it yet. ## Outcomes: included is not "it worked" [#outcomes-included-is-not-it-worked] LEZ 0.3 includes failed transactions (and charges their fee). So the wallet proves each result from chain state and reports an `outcome`: | `outcome` | Evidence | Your app shows | | --------- | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | | `success` | Your own account moved by exactly the amount, or the record your call writes exists | Done | | `failure` | Included, and the effect provably didn't happen | The error and a retry | | `unknown` | Included, but the wallet can't prove it either way | "Included, not confirmed yet", then re-read the state you expected to change | `outcomeSource` says how it was decided (`own-account-invariant`, …). ## Proving time [#proving-time] Private transactions need a zero-knowledge proof, made on the user's machine so keys never leave it. | Machine | Time per proof | Peak memory | | --------------------- | ---------------------------------- | ----------- | | Apple M-series laptop | \~4.5 minutes (measured 267–337 s) | \~4.3 GB | The wallet shows progress and lets the user close the sheet; the proof keeps running. Only one proof runs at a time. ## Amounts: LGO and lepta [#amounts-lgo-and-lepta] The native token is LOGOS, symbol **LGO**: 1 LGO = 10^9 lepta. LEZ is the name of the execution zone (the network), not of the token. | Where | Unit | | -------------------------------------------------------------------------------- | ---------------------------------------------- | | The wire, JSON, the SDK (`transfer`, `getWalletBalance`, `requestFunds` results) | lepta, as strings | | The wallet, the apps, the CLI's human output | LGO, up to 9 decimals | | The CLI's `--json` output | lepta | | The CLI's `send --amount` | lepta (`42`) or LGO (`2.5`, `2.5LGO`, `3 LGO`) | LGO is shown with up to 9 decimals, nothing rounded and trailing zeros trimmed: 1 lepta is `0.000000001 LGO`. This follows the official Logos UI convention (`logos-blockchain-ui` PR #69). In an app, show lepta with `kit.sdk.formatUnits(amount, 9)` and parse what the user typed with `kit.sdk.parseUnits(text, 9)`. ## Fees [#fees] Fees are paid in the native token: lepta on the wire, LGO on screen. Private transactions are fee-exempt in LEZ 0.3. A public one pays ``` min(cycles used, gas limit) × execution base fee + signed bytes × storage base fee + tip ``` The wallet shows three figures: | Where | Figure | Source | | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | | Approval sheet, "Network fee" | The fee at today's base fees. A native transfer runs outside the zkVM at zero cycles, so its fee is exact: bytes × storage base fee + tip. A program call shows **up to**, pricing its cycles at the gas limit, because they're only known once it runs | `getFeeState` when the review is built | | Approval sheet, technical details | The **fee cap** (`maxFee`) the payer signs, the gas limit and the base fee | The transaction itself | | After inclusion (public native sends) | The **fee paid** | How much the sender's balance dropped beyond the amount | The LEZ 0.3 RPC doesn't report gas used: `getTransaction` returns the transaction and its block, and there is no simulate method. The paid fee is therefore read from the sender's balance. If anything else moves that balance in the meantime, such as an incoming payment or another send, the wallet shows no fee paid rather than guess. The approval binds to the cap, so a base fee rise between review and inclusion can never charge more than the user signed for. ## Re-checks at signing [#re-checks-at-signing] Between approval and signing, time passes (sometimes minutes of proving). Just before signing, the wallet re-checks what the approval depended on: the signer nonces, the program's image, and, for testimonial posts, which stats page is open. If something moved, it refuses with `6106 StaleApproval` (propose again), or, where the effect is provably identical, rebuilds the same post ([details](/docs/wallet/security)). # Installation (/docs/getting-started/installation) ## 1. The wallet [#1-the-wallet] Your app talks to a wallet the user already has. Install Logos Kit in Basecamp from its catalog: ### Add the catalog [#add-the-catalog] In Basecamp: **Settings → Package Repositories → Add**, and paste: ```text https://raw.githubusercontent.com/logos-kit/logos-kit-modules/refs/heads/main/logos-repo.json ``` ### Install Logos Kit Wallet [#install-logos-kit-wallet] Open **Applications**, pick **Logos Kit Wallet** and press **Install**. The wallet core (`logos_kit_wallet`) comes with it. The **Logos Kit Testimonials** and **Logos Kit Faucet** apps are in the same catalog. ### Create a wallet [#create-a-wallet] Open it, choose **Create wallet**, and write down the recovery phrase. The wallet starts on the official [LEZ testnet](/docs/concepts/networks); **Get test LGO** fills a public account from Logos Kit's testnet faucet. Every package is signed by the Logos Kit release key. The catalog builds for macOS (Apple silicon) and Linux (x86-64 and arm64). Prefer a terminal? The [CLI](/docs/wallet/cli) is the same wallet. ## 2. The SDK [#2-the-sdk] Start from the template, which already has the SDK, the UI kit and the wallet's contract: ```sh nix flake init -t github:logos-kit/logos-kit#dapp ``` Or copy `sdk/qml/LogosKit/` (and optionally `sdk/qml/LogosKitUi/`) from the repo into your module's `qml/` folder, and add the wallet to your `metadata.json`: ```json "dependencies": ["logos_kit_wallet"], "uses": [ { "intent": "lez.wallet.connect" }, { "intent": "lez.transaction.send" } ] ``` Every intent you call must be listed under `uses`, as an object. Otherwise the shell answers `not_declared`. npm pnpm yarn bun ```bash npm i @logos-kit/client @logos-kit/codec ``` ```bash pnpm add @logos-kit/client @logos-kit/codec ``` ```bash yarn add @logos-kit/client @logos-kit/codec ``` ```bash bun add @logos-kit/client @logos-kit/codec ``` All four packages are on npm under MIT OR Apache-2.0. New versions publish from GitHub Actions through npm trusted publishing, with provenance. They're Node and transport tooling (typed chain reads, encoding, the Basecamp module bridge), written in QML-safe ES2017 with no native `BigInt`. A browser or React Native wallet connector is planned, not shipped. See [TypeScript client](/docs/sdk/client). ## Requirements [#requirements] | For | You need | | ----------------------- | ------------------------------------------------------------------------------------- | | Using the wallet | Basecamp (macOS arm64 or Linux), or the `logos-kit` CLI | | Building a Basecamp app | [Nix](https://nixos.org/download) with flakes | | Private transfers | \~4.3 GB of free memory while proving; about 4–7 minutes per proof on an M-series Mac | | Building from source | Rust 1.98 (`rust-toolchain.toml`), Nix, and Docker for reproducible program builds | # Quickstart (/docs/getting-started/quickstart) You'll install the wallet from its catalog, make a first send in it, then build the template app and use it to connect, get test funds and send. | Part | Time | | -------------------------------------- | ------------------------------------------------------------------------------------------- | | 1. Wallet from the catalog, first send | about 5 minutes | | 2. Your app from the template | 5–10 minutes with Nix installed; the first `nix build` can take 20+ minutes on a cold cache | | Private transfers (optional) | 4–7 minutes per proof on an M-series Mac | **You need:** Basecamp (macOS on Apple silicon, or Linux x86-64 or arm64) and, for part 2, [Nix](https://nixos.org/download) with flakes enabled. ## 1. The wallet [#1-the-wallet] ### Add the Logos Kit catalog [#add-the-logos-kit-catalog] In Basecamp: **Settings → Package Repositories → Add**, and paste exactly: ```text https://raw.githubusercontent.com/logos-kit/logos-kit-modules/refs/heads/main/logos-repo.json ``` ### Install Logos Kit Wallet [#install-logos-kit-wallet] Open **Applications**, pick **Logos Kit Wallet**, press **Install**. Basecamp pulls the core module (`logos_kit_wallet`) with it. The wallet appears in the sidebar. ### Create a wallet [#create-a-wallet] Open it and choose **Create wallet**. Pick a password, then write down the recovery phrase: it's shown once. The wallet opens on the official **LEZ testnet** (`lez:testnet`), which runs LEZ `v0.3.0`. ### Get test funds, then send [#get-test-funds-then-send] On the home screen, press **Test LGO**. Logos Kit's testnet faucet sends 1 LGO to your public account. Then **Send** some to a second public account (**account menu → + Public account**) and approve it. On the testnet it can take a minute or more to land.
## 2. Your app [#2-your-app] ### Start from the template [#start-from-the-template] ```sh mkdir my-app && cd my-app nix flake init -t github:logos-kit/logos-kit#dapp ``` You get a `ui_qml` module: `metadata.json`, `qml/Main.qml`, the SDK in `qml/LogosKit/`, the UI kit in `qml/LogosKitUi/`, and the wallet's contract in `logos_kit_wallet.lidl`. ### Name it [#name-it] In `metadata.json`, set `name` (lowercase with `_`), `display_name` and `description`, and replace `src/icons/icon.png`. Keep every intent you use under `uses`, as objects: ```json "uses": [ { "intent": "lez.wallet.connect" }, { "intent": "lez.transaction.send" }, { "intent": "lez.wallet.request_funds" } ] ``` ### Build and install [#build-and-install] ```sh nix build .#lgx-portable ``` Install `result/*.lgx` from Basecamp's **Package Manager → Install from file**, or with `lgpm install --file result/*.lgx`. Your app appears in the sidebar. ### Connect, get funds, send [#connect-get-funds-send] Open your app and press **Connect wallet**. Basecamp routes the request to Logos Kit, and the wallet asks which account to share. Then **Get test funds** (the wallet shows its faucet sheet) and send. The template's code for the send: ```qml import "LogosKit" LogosKit { id: kit; visible: root.visible } kit.api.transfer(root.account, to, "42").then(function (r) { kit.api.watchTransaction(r.handle, function (s) { if (s.lifecycle !== "included") return root.phase = s.outcome === "success" ? "done" : s.outcome === "failure" ? "failed" : "unconfirmed" // re-check the balance before calling it done }) }) ``` Amounts are lepta, as strings (1 LGO = 10^9 lepta): `"42"` is 0.000000042 LGO. Turn what the user types in LGO into lepta with `kit.sdk.parseUnits(text, 9)`, and show amounts with `kit.sdk.formatUnits(amount, 9)`. The wallet shows the approval with your app's icon and name. Once it lands, the template shows the receipt. ## If something goes wrong [#if-something-goes-wrong] | Symptom | Cause | Fix | | ------------------------------------------------- | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | | Connect fails with `not_declared` | The intent isn't in `metadata.json` `uses` (or it's a bare string) | Add it as `{ "intent": "…" }`, rebuild, reinstall | | Connect does nothing, or fails with `unavailable` | No wallet provides the intent: Logos Kit isn't installed, or the core module didn't load | Install Logos Kit Wallet from the catalog; restart Basecamp; check it opens | | Basecamp asks which wallet to use | More than one wallet provides the intent | Pick Logos Kit | | `4900 Disconnected` | The wallet is locked or not set up | Open Logos Kit Wallet and unlock it | | The wallet shows an offline banner, or `4901` | The sequencer isn't reachable | Check the network pill in the wallet; the testnet's status is on this site's home page; retry later | | `rate_limited` on test funds | One drop per account per hour, five per IP | Wait for the countdown | | Included, but `unknown` | The wallet couldn't prove the effect | Re-read the balance; show "not confirmed yet" until it moves | More in [Troubleshooting](/docs/help/troubleshooting). ## Develop against a local chain [#develop-against-a-local-chain] The SDK follows the wallet's network. Run `e2e/standalone.sh` from a checkout of this repo (a local sequencer on `:3040`), switch the wallet to **LEZ local** (**Settings → Network**), and your app follows. `kit.chain` tells you where you are. ## Next [#next] * [Send a transaction](/docs/guides/send): the full lifecycle and every ending. * [The testimonial app](/docs/guides/testimonial): a complete app on the SDK. * [QML SDK](/docs/sdk/qml): every call. * [Errors](/docs/reference/errors): what can go wrong and what to show. # Connect a wallet (/docs/guides/connect) Connecting opens the wallet's connect sheet. The user picks which accounts to share and sees what your app will be able to do. ## Connect [#connect] ```qml LogosKit { id: kit visible: root.visible // Ready (or rebuilt for another network): restore without prompting. onApiChanged: if (api) api.getSession().then(function (s) { if (s) root.session = s }) } function connect() { kit.api.connect({ accountKinds: ["public"] }).then(function (s) { root.session = s // { sessionId, accounts[], chains, capabilities } root.account = s.accounts[0].address }, function (e) { if (!kit.sdk.isUserRejection(e)) root.error = e.message }) } ``` | Parameter | Default | | | -------------- | ------------------------ | ------------------------------------------------------------------------------------------------------- | | `accountKinds` | `["public"]` | Add `"private"` to allow private accounts. The user ticks each one explicitly; you get `pvt_…` handles. | | `capabilities` | the wallet's default set | See [Permissions](/docs/concepts/permissions). | | `signIn` | none | A sign-in request (SIWE-style) to sign in the same sheet. | ## Restore on start [#restore-on-start] `getSession()` never prompts. Call it when `kit.api` becomes ready: if the user connected before, you get the session back; otherwise `null`, and you show your **Connect** button. ## When the user says no [#when-the-user-says-no] A cancelled sheet rejects with `4001`. It's not an error: check `kit.sdk.isUserRejection(e)` and show nothing. Other failures are in [Errors](/docs/reference/errors). ## States to design [#states-to-design] | State | Show | | ------------------------ | ----------------------------------------------------------------------------------- | | `kit.api === null` | A skeleton: the SDK is loading | | No session | A **Connect** button and what your app does | | Connecting | The button busy; the wallet is showing its sheet | | Connected, empty account | Balance `0` and an in-flow **Get test funds** ([faucet guide](/docs/guides/faucet)) | # Handle errors (/docs/guides/errors) Every rejection is a `LezError`: `{ code, message, data? }`. Most need one of four responses. | Situation | Codes | Do | | -------------------------- | ---------------------- | ------------------------------------------------------------------------------ | | The user said no | `4001` | Show nothing. `kit.sdk.isUserRejection(e)` | | The wallet is still busy | `6107`, `6108` | Say "Waiting for your wallet"; see [slow approvals](/docs/guides/late-results) | | Access changed | `4100`, `4900`, `4902` | Ask to connect again, unlock, or switch network | | The chain moved or refused | `6106`, `6102`, `6103` | Propose again (fresh state) | ```qml function show(e) { if (kit.sdk.isUserRejection(e)) return switch (e.code) { case kit.sdk.ErrorCode.Timeout: root.phase = "waiting"; return case kit.sdk.ErrorCode.Unauthorized: root.session = null; root.hint = "Connect again"; return default: root.error = e.message } } ``` Show the wallet's `message` as plain text (`textFormat: Text.PlainText`). The UI kit's `Txt` does that by default. A transaction that **lands but fails** isn't a rejection: it resolves, then its status ends `included` with `outcome: "failure"`. See [Send a transaction](/docs/guides/send). Every code is listed in [Error codes](/docs/reference/errors). # Worked example: faucet (/docs/guides/faucet) **Logos Kit Faucet** (`modules/logos_kit_faucet`) gets test LGO into any account the user shares. It gives each faucet outcome its own state, including the ones where it can't tell yet, and the private path, where funds arrive with a proof. On the official testnet the wallet uses Logos Kit's drip faucet: 1 LGO per request, each account once an hour, each IP five times an hour ([Networks](/docs/concepts/networks#faucet)). ## 1. Connect, both kinds [#1-connect-both-kinds] ```qml kit.api.connect({ accountKinds: ["public", "private"] }) ``` Private accounts need their own consent in the wallet's connect sheet. The app gets a per-app handle for each (`pvt_…`); it never learns the account itself. ## 2. Pick, then request [#2-pick-then-request] ```qml kit.api.requestFunds(account).then(function (r) { if (r.status === "funded") r.shieldHandle ? follow(r.shieldHandle) : done(r.amount) else if (r.status === "rate_limited") countdown(r.retryAfterSeconds) else if (r.status === "outcome_unknown") watchBalance(r.fundedAccount || account) else declined(r.reason) }) ``` | Outcome | What the app shows | | ----------------- | ------------------------------------------------------------------------------------------------------- | | `funded` | the amount (lepta: show `kit.sdk.formatUnits(r.amount, 9)` LGO), the account, a link to the transaction | | `rate_limited` | a countdown on the button, which reopens at zero | | `outcome_unknown` | "Checking…": it watches the balance instead of asking again (a second request could pay twice) | | `rejected` | the faucet's reason | ### When the outcome is unknown [#when-the-outcome-is-unknown] The faucet answered ambiguously (a timeout, a dropped connection), so the funds may or may not have landed. The app never re-requests on its own. Instead it: 1. Re-reads the paid account's balance on a timer, for a bounded time. Each attempt counts, including failed reads, so an unreachable node can't keep it checking forever. 2. Shows **Funds arrived** as soon as the balance goes up. 3. Otherwise ends in an **unresolved** state: "We couldn't confirm the payment", with **Check again** (one more balance read) and a plain note not to request again until the check settles. For a private target, the account to watch is **not** the private one. The faucet only pays public accounts, so it paid `r.fundedAccount`, one of the user's public accounts. The app watches that account and says so ("The faucet paid Public account 1"). If the funds turn up there, the user can move them into the private account from the wallet.
## 3. The private path [#3-the-private-path] The faucet only pays public accounts. For a private target the wallet funds one of the user's public accounts (`fundedAccount`), then queues a transfer into the private account (a *shield*), which the user approves in the wallet. The app follows it with the returned `shieldHandle`: ```qml kit.api.watchTransaction(r.shieldHandle, function (s) { // awaiting_approval → proving (minutes, on the user's machine) → submitted → included if (s.lifecycle !== "included") return if (s.outcome === "success") done() // the private balance moved else if (s.outcome === "failure") failed(s) // funds are still on fundedAccount else unconfirmed(s) // included, outcome unconfirmed }) ``` The app shows four steps (paid, approve, proving with a timer, included). Only `outcome === "success"` shows Done. An included shield with outcome `unknown` gets its own **Included, outcome unconfirmed** state, which says the funds are either in the private account or still on the public one, and offers a balance check. If the user declines the move, the app says plainly that the funds stay on the public account.
## Run it yourself [#run-it-yourself] ```sh e2e/faucet-app.sh # public claim, rate limit, private claim through a shield ``` # Follow the network (/docs/guides/follow-network) The user picks the network in the wallet (**Settings → Network**). The SDK follows it: on start, when your app is shown, and every 5 seconds while it's visible. ```qml LogosKit { id: kit visible: root.visible onApiChanged: if (api) root.reset() // a new network: start over } Text { text: kit.chain } // "lez:testnet", "lez:preview", "lez:local" ``` When the network changes, `kit.api` is **rebuilt**. Anything your app holds from the old network (session, balances, handles, watchers) is stale: ```qml property int epoch: 0 function reset() { epoch++ if (watcher) watcher.stop() session = null; balance = ""; phase = "idle" } // Drop answers that arrive after the network changed. function live(f) { var ep = epoch return function (v) { if (ep === epoch) return f(v) } } kit.api.getWalletBalance(account).then(live(function (b) { balance = b.amount })) ``` Always read `kit.api` fresh; never keep a copy. To pin a network instead (for a demo, say), set `followWallet: false` and `chain`. Calls then fail with `4902 UnknownChain` while the wallet is elsewhere. Networks and their programs are listed in [Networks](/docs/concepts/networks). # Slow approvals (/docs/guides/late-results) A user-facing call waits 45 seconds for the wallet. Then it rejects with `6108 Timeout`, but **the wallet may still be showing the request**. The user can still approve it, so a timeout doesn't mean nothing was sent. The SDK keeps new requests blocked (`6107 RequestPending`) until the wallet really answers, and delivers that answer on `lateResult`: ```qml LogosKit { id: kit onLateResult: function (intent, ok, data) { if (intent !== kit.sdk.INTENTS.sendTransaction || root.phase !== "waiting") return if (ok) root.follow(data.handle) // the same path as an on-time approval else if (!kit.sdk.isUserRejection(data)) root.error = data.message } } function send() { kit.api.transfer(from, to, amount).then(function (r) { root.follow(r.handle) }, function (e) { if (e.code === kit.sdk.ErrorCode.Timeout) root.phase = "waiting" // not an error else if (!kit.sdk.isUserRejection(e)) root.error = e.message }) } ``` Show "Waiting for your wallet" in the `waiting` state, not an error. # Pattern: other Logos modules (/docs/guides/other-modules) A Basecamp app isn't limited to the wallet. The same `logos` bridge that carries LWS-0 calls to `logos_kit_wallet` reaches any other **core** module. This example pays with the wallet, then saves a JSON receipt of the payment to **Logos Storage** through `storage_module`, and shows the receipt's CID. **Not yet run in Basecamp.** This is a pattern, not one of the shipped apps, and unlike the testimonial and faucet examples it hasn't been exercised end to end. The storage calls follow `storage_module` **3.0.0-rc1** (`logos-co/logos-storage-module`, `src/storage_module_plugin.h`). Check the header of the version your users run: a core module's method names and arguments are its own contract. ## 1. Declare both modules [#1-declare-both-modules] List every core module the app calls in `metadata.json`, next to the wallet intents it uses: ```json title="metadata.json" { "name": "receipts_app", "type": "ui_qml", "view": "qml/Main.qml", "dependencies": ["logos_kit_wallet", "storage_module"], "uses": [ { "intent": "lez.wallet.connect" }, { "intent": "lez.transaction.send" } ] } ``` ## 2. How a core module answers in QML [#2-how-a-core-module-answers-in-qml] Wallet calls go through `kit.api`, which parses the answers for you. For any other core module you call the bridge directly, and the answer is a JSON **string**. `storage_module` returns a `StdLogosResult`, which arrives as `{"success": bool, "value": …, "error": "…"}`. Plain `bool` returns arrive as the bare literal (`true`). If the bridge itself fails (the module isn't installed, or the call timed out), the payload is `{"error": "…"}` with no `success` field. A small wrapper turns that into a promise-shaped callback: ```qml // Call storage_module; cb(error, value). function storage(method, args, cb) { logos.callModuleAsync("storage_module", method, args, function (payload) { var r try { r = JSON.parse(payload) } catch (e) { return cb("bad answer from storage_module") } if (r === true || r === false) return cb(null, r) // bool methods if (r && r.success === true) return cb(null, r.value) cb((r && r.error) || "storage_module failed") }, 20000) } ``` ## 3. Check the storage node [#3-check-the-storage-node] The Storage node runs inside `storage_module`, and the user starts it (for example from the Logos Storage app). Check before offering the button: ```qml storage("isRunning", [], function (err, running) { root.storageReady = !err && running === true }) ``` When it isn't running, say so ("Start Logos Storage to save receipts") rather than failing later. You can also call `start()` yourself: it returns at once, and the node reports readiness on the `storageStart` event. ## 4. Pay through the wallet [#4-pay-through-the-wallet] This is the normal SDK flow. The wallet shows its own approval sheet: ```qml kit.api.transfer(from, to, amount).then(function (r) { root.handle = r.handle kit.api.watchTransaction(r.handle, function (s) { if (s.lifecycle !== "included") return if (s.outcome === "success") saveReceipt(s) else root.problem = "The payment didn't go through, so there's no receipt to save." }) }, function (e) { if (!kit.sdk.isUserRejection(e)) root.problem = e.message }) ``` Save a receipt only for `outcome === "success"`. An `unknown` outcome means the wallet couldn't confirm the effect, so the receipt would be a guess. ## 5. Upload the receipt [#5-upload-the-receipt] For data that isn't on disk, `storage_module` has a manual upload session: `uploadInit(filename, chunkSize, advertise)` returns a session id, `uploadChunk(sessionId, chunk)` sends bytes, and `uploadFinalize(sessionId)` returns the CID. `uploadChunk` is asynchronous: it reports on the `storageUploadProgress` event, so subscribe first and finalize once the chunk has been acknowledged. ```qml Component.onCompleted: logos.onModuleEvent("storage_module", "storageUploadProgress") property string uploadSession: "" function saveReceipt(s) { var receipt = JSON.stringify({ chain: s.chain, txHash: s.txHash, block: s.block ? s.block.id : null, from: from, to: to, amount: amount, }) // advertise = false: don't announce it to the network or serve it to peers. storage("uploadInit", ["receipt-" + s.txHash + ".json", 65536, false], function (err, session) { if (err) return root.problem = "Couldn't start the upload: " + err root.uploadSession = session storage("uploadChunk", [session, receipt], function (err2) { if (err2) { storage("uploadCancel", [session], function () {}); root.problem = err2 } }) }) } Connections { target: logos function onModuleEventReceived(moduleName, eventName, data) { if (moduleName !== "storage_module" || eventName !== "storageUploadProgress") return var e = JSON.parse(data[0]) if (e.sessionId !== root.uploadSession) return if (!e.success) return root.problem = e.error storage("uploadFinalize", [root.uploadSession], function (err, cid) { root.uploadSession = "" if (err) root.problem = err else root.receiptCid = cid // show it, let the user copy it }) } } ``` Each storage event carries one JSON string in `data[0]`. The chunk here is a small text receipt; for larger data, send several chunks (up to `chunkSize` bytes each) and finalize after the last one is acknowledged. ## 6. States to show [#6-states-to-show] | State | When | | ----------------------------- | ---------------------------------------------------------------------------------------------------------- | | Storage off | `isRunning` is `false`: the Pay button still works, and the receipt option explains how to turn Storage on | | Paying | between `transfer()` and the wallet's answer (the wallet shows the sheet) | | Waiting for the block | watching the handle | | Saving receipt | between `uploadInit` and `uploadFinalize` | | Saved | the CID, with a copy button | | Payment failed or unconfirmed | no upload; say why | | Upload failed | the payment still went through: say so, and offer **Save again** | ## What stays private [#what-stays-private] A public transfer is already public on chain, so its receipt reveals nothing new. Don't upload anything about **private** accounts: the app only ever sees a per-app `pvt_…` handle for them, and a receipt linking that handle to an amount would leak what the wallet keeps private. For personal data, encrypt it on the user's device before `uploadChunk`: Logos Storage stores what you give it. # Private transfers (/docs/guides/private) Private transfers use the same calls as public ones. What changes is time: the wallet proves them on the user's machine first. ```qml // Into the user's own private account (a shield): kit.api.transfer(publicAccount, privateHandle, "5000") ``` The route (shield, deshield, private-to-private) follows from the accounts; see [Accounts](/docs/concepts/accounts). ## Design for minutes, not seconds [#design-for-minutes-not-seconds] * `transfer()` resolves at **approval**. Proving comes after. * The status goes `building → proving → signing → submitted → included`, and `status.proving.phase` updates while it proves. * Proving takes about 4–7 minutes on an M-series Mac and \~4.3 GB of memory. * The user may close your app: the proof keeps running in the wallet. When your app opens again, follow the same handle (keep it with your state). ## Confirming the outcome [#confirming-the-outcome] For private routes, the wallet confirms the outcome from **its own** account: the private balance moved by exactly the amount. A shield into the user's account usually ends `success`. If a note arrived at the same time, it may end `unknown`: re-read the balance. ## Reading private balances [#reading-private-balances] `getWalletBalance(pvtHandle)` answers only if the user shared that private account with your app. While the wallet catches up with the chain, `synced` is `false`; show "Syncing" rather than a stale number. # Read balances and data (/docs/guides/read) Reads are module calls: they never open the wallet and return quickly. ## Balances [#balances] ```qml kit.api.getWalletBalance(root.account).then(function (b) { root.balance = b.amount // "1000000000": lepta, so 1 LGO (a string: u128 never becomes a JS number) root.synced = b.synced // false while a private account is still syncing }) // A token: pass its definition account. kit.api.getWalletBalance(root.account, tokenDefinition).then(function (b) { … }) ``` | Field | | | ----------- | ---------------------------------------------------------------------------------------------------------------------------- | | `amount` | The raw integer amount, as a decimal string: lepta for the native token (1 LGO = 10^9 lepta), a token's base units otherwise | | `asset` | The token definition, or native | | `synced` | Private accounts: whether the wallet has caught up with the chain | | `asOfBlock` | The block the balance is from | Private handles (`pvt_…`) work too, if the user shared that account. Format with the SDK's helpers, never with JS numbers: ```qml kit.sdk.formatUnits(b.amount, 9) + " LGO" // "1000000000" → "1 LGO" kit.sdk.parseUnits("2.5", 9) // what the user typed in LGO → "2500000000" lepta kit.sdk.formatUnits("1500000", 6) // a token with 6 decimals: "1.5" ``` The native token is LGO, with 9 decimals. `formatUnits` shows up to 9, rounds nothing and trims trailing zeros: 1 lepta is `0.000000001` LGO. ## Public account data [#public-account-data] ```qml kit.api.readAccount(accountId, programId).then(function (a) { // { nonce, data }: the account's data under that program, as bytes }) ``` ## The network [#the-network] ```qml kit.chain // "lez:testnet", follows the wallet kit.api.getChainId() // ask the wallet directly ``` See [Follow the network](/docs/guides/follow-network). ## In TypeScript [#in-typescript] The same reads work from Node against any sequencer. From a browser they work only against endpoints that send CORS headers. The official testnet's RPC doesn't, so Logos Kit runs a relay for it that does: `https://lez-testnet.84.46.247.92.sslip.io`. ```ts import { createClient, http, nodeActions } from '@logos-kit/client' const node = createClient({ transport: http('https://lez-testnet.84.46.247.92.sslip.io'), // from Node, https://testnet.lez.logos.co works too chain: 'lez:testnet', }).extend(nodeActions) const height = await node.getBlockNumber() ``` See [TypeScript client](/docs/sdk/client). # Send a transaction (/docs/guides/send) Sending has two halves: the user **approves** in the wallet, then the transaction **lands**. Your app sees both. ### Propose [#propose] ```qml kit.api.transfer(root.account, to, "42").then(function (r) { root.follow(r.handle) // resolves once approved, before it lands }, function (e) { if (e.code === kit.sdk.ErrorCode.Timeout) root.phase = "waiting" // see late results else if (!kit.sdk.isUserRejection(e)) root.error = e.message }) ``` `transfer(from, to, amount, token?)` covers the native token (LGO) and tokens. A native amount is lepta, as a string: `"42"` is 0.000000042 LGO, and `kit.sdk.parseUnits(text, 9)` turns what the user typed in LGO into lepta. For any other program, build a call and use `sendCall`: ```qml var call = kit.sdk.nativeTransfer(from, to, "42") // or your own ProgramCall kit.api.sendCall(from, call).then(function (r) { root.follow(r.handle) }) ``` ### Follow [#follow] ```qml function follow(handle) { root.phase = "sending" root.watcher = kit.api.watchTransaction(handle, function (s) { root.status = s // lifecycle, block, txHash, proving, outcome… if (s.lifecycle === "included") { root.phase = s.outcome === "success" ? "done" : s.outcome === "failure" ? "failed" : "unconfirmed" } else if (["rejected", "dropped", "expired"].indexOf(s.lifecycle) >= 0) { root.phase = "failed" } }) } ``` The watcher stops by itself at the first final lifecycle. Call `root.watcher.stop()` if the user leaves. On the official testnet a transaction can take a minute or more to be included. ### Show the ending [#show-the-ending] | Ending | Show | | ---------------------------------- | ------------------------------------------------------------------------------------ | | `included` + `success` | Done: the receipt, and an explorer link (`kit.api.openExplorer({ txHash })`) | | `included` + `failure` | It was refused on chain; the fee was charged. Offer a retry. | | `included` + `unknown` | "Included, not confirmed yet". Re-read the balance or record you expected to change. | | `rejected` / `dropped` / `expired` | It didn't happen; `status.error` says why | The full lifecycle and outcome rules are in [Transactions](/docs/concepts/transactions). # Worked example: testimonials (/docs/guides/testimonial) **Logos Kit Testimonials** (`modules/logos_kit_testimonial`) posts a short testimonial to a LEZ program that stores it on chain, and shows what others wrote. It's the SDK's reference integration: every step of connect → account selection → approval → transaction, with the states around it. ## The program [#the-program] A standard LEZ program (`programs/testimonial`) keeps one record per `(submission, author)` and paged stats with monthly counts. A post carries the text (≤ 280 bytes), an optional username (≤ 32 bytes) and the submission id `LP-0021/logos-kit`, and the chain checks its timestamp against the block time. Each account posts once. The author signs and pays the fee, so a post is always public. On the official testnet it's deployed as an immutable program, `5YoH3xjhgeKt2mcJXW7c31bqDNCWWA4CRJxVdvzFvVef` (`TESTIMONIAL_PROGRAMS["lez:testnet"]`). Posts there count for LP-0021 adoption; posts on the preview network don't. ## 1. Connect: the user picks the account [#1-connect-the-user-picks-the-account] ```qml kit.api.connect({ accountKinds: ["public"] }).then(useSession, function (e) { root.note = kit.sdk.isUserRejection(e) ? "" : errText(e) }) ``` The shell routes `lez.wallet.connect` to the wallet, which asks which account to share. The app only ever sees accounts the user picked. On start, the app calls `getSession()` to restore an earlier connection without a prompt. ## 2. Read before asking [#2-read-before-asking] Before showing the composer, the app reads three things through the wallet: ```qml kit.api.getWalletBalance(account) // enough for the fee? kit.api.readAccount(account, programId) // nonce: has it been used? kit.api.getTestimonial(programId, account) // already posted? ``` That decides the state: **already posted** (show it), **no funds** (offer test funds in place: `kit.api.requestFunds(account)`), a **fresh account** (a gentle nudge: posts from accounts with real use count for more), or the composer. ## 3. Compose, and validate like the chain does [#3-compose-and-validate-like-the-chain-does] The app checks what the program would refuse, before the user approves anything: byte length (not characters), blank text, control and direction-override characters. It also requires the text to name the wallet, since testimonials count for the wallet they name. ## 4. Propose: the user approves in the wallet [#4-propose-the-user-approves-in-the-wallet] ```qml kit.api.postTestimonial({ program: programId, author: account, text: text, username: name }) .then(function (r) { root.watcher = kit.api.watchTransaction(r.handle, onStatus) }) ``` `postTestimonial` finds the open stats page and builds the call. The wallet shows the approval: the program (with its source-verification and upgrade status), the fee, and the decoded post. The promise resolves once the user approves, with a **handle**; inclusion comes after. ## 5. Follow the transaction [#5-follow-the-transaction] ```qml function onStatus(s) { if (s.lifecycle === "included" || s.lifecycle === "finalized") { if (s.outcome === "success") done() else if (s.outcome === "failure") recheckAfterReject(s) else confirm() // included, outcome unknown: the record decides } else if (s.lifecycle === "rejected" || s.lifecycle === "dropped" || s.lifecycle === "expired") { recheckAfterReject(s) } } // Unknown outcome: read the author's record. Our post there means it landed. function confirm() { kit.api.getTestimonial(programId, account).then(function (t) { root.phase = t && t.text === root.text ? "done" : "unconfirmed" }, function () { root.phase = "unconfirmed" }) } ``` Only `success`, or the record itself, shows Done. Otherwise the app shows **Not confirmed yet** with a re-check, never a second post. A rejected post can still mean the account already posted (another device), so the app reads the record again before calling it a failure. When it's done, the app shows the transaction and block, and `kit.api.openExplorer({ txHash })` opens it in the user's browser (the QML sandbox can't open links; the wallet builds the URL).
## The feed [#the-feed] ```qml kit.api.getTestimonials(programId, { limit: 25 }) // { count, monthly, latest } ``` Everything comes from chain data: the count, this month's tally and the newest posts, decoded from the program's accounts. ## Run it yourself [#run-it-yourself] ```sh just e2e-testimonial-app # local sequencer, program deploy, the whole flow in the dev harness ``` # Tokens (/docs/guides/tokens) LEZ tokens live in the built-in token program. A token is identified by its **definition account**. ## Balance and transfer [#balance-and-transfer] ```qml kit.api.getWalletBalance(account, definition).then(function (b) { root.tokens = b.amount }) kit.api.transfer(from, to, "250", definition).then(function (r) { root.follow(r.handle) }) ``` Public and private accounts both hold tokens; private token transfers prove like native ones. The approval sheet shows the token, the amount and the destination. A public token send is confirmed from the sender's token balance (it drops by exactly the amount; fees are paid in LGO, the native token). ## Build the call yourself [#build-the-call-yourself] ```qml var call = kit.sdk.tokenTransfer(from, to, definition, "250") // kind defaults to "fungible" kit.api.sendCall(from, call) ``` ## Create a test token [#create-a-test-token] From the CLI (the whole supply goes to `--holder`): ```sh logos-kit token create --name KIT --supply 1000000 --holder logos-kit token list ``` Amounts are raw integers, as strings: a token's base units (the CLI's `--amount` with `--token` takes a whole number of them). If your token defines decimals for display, `kit.sdk.formatUnits(amount, decimals)` formats them. The native token is different: its amounts are lepta, shown as LGO with `kit.sdk.formatUnits(amount, 9)` (1 LGO = 10^9 lepta). # FAQ (/docs/help/faq) No. Logos Kit runs on LEZ testnets and hasn't been audited. Use test funds only. In the wallet's encrypted vault on your machine (Argon2id and XChaCha20-Poly1305). No app, and no Logos Kit server, ever sees them. See the [security model](/docs/wallet/security). It's proved on your own machine with RISC Zero, so your keys never leave it. That takes minutes on a laptop. Public transfers take seconds. The official LEZ testnet (`https://testnet.lez.logos.co`, LEZ `v0.3.0`), by default in the wallet, the CLI and the SDK. [Details](/docs/concepts/networks). LGO is the symbol of LOGOS, the native token. LEZ is the name of the execution zone (the network), not of the token. 1 LGO = 10^9 lepta: the wire, JSON and the SDK use lepta, and the wallet, the apps and the CLI show LGO with up to 9 decimals, nothing rounded. [Details](/docs/concepts/transactions#amounts-lgo-and-lepta). Logos Kit's own public LEZ 0.3 network (`v0.3.0-rc1`, real proofs), from before the official testnet moved to 0.3. It's no longer the default; it stays listed for wallets that saved it, as a legacy rehearsal network. [Details](/docs/concepts/networks). No. The prize counts testimonials on the official LEZ testnet, where the program is deployed as `5YoH3xjhgeKt2mcJXW7c31bqDNCWWA4CRJxVdvzFvVef`. Not yet. Today `@logos-kit/client` is Node and transport tooling: typed chain reads, encoding, and the Basecamp module bridge. It has no browser wallet connector. It can read the testnet from a browser through Logos Kit's relay, `https://lez-testnet.84.46.247.92.sslip.io`, which sends CORS headers (the official RPC doesn't). A web connect kit (React), React Native, and an embedded passkey wallet are planned, not shipped. Your users need a wallet that answers the LWS-0 intents; Logos Kit Wallet is one. During development you can also test against the conformance fake (`modules/logos_kit_wallet_fake`, `just conformance `). Yes, MIT or Apache-2.0, at [github.com/logos-kit/logos-kit](https://github.com/logos-kit/logos-kit). # Troubleshooting (/docs/help/troubleshooting) The intent isn't listed in your `metadata.json` `uses`. Each entry must be an object: `{ "intent": "lez.wallet.connect" }`, not a bare string. Rebuild and reinstall the module. A wallet sheet is still open for your app, or an earlier request timed out and the wallet hasn't answered yet. Finish or cancel it in the wallet. Handle the late answer with `onLateResult` ([guide](/docs/guides/late-results)). The SDK isn't ready yet, or it's being rebuilt after a network switch. Start work in `onApiChanged: if (api) …`, and read `kit.api` fresh each time. The wallet is locked, or not set up. Ask the user to open Logos Kit Wallet and unlock it. Your proposal names a network the wallet isn't on. With `followWallet` (the default) this only happens during a switch: retry after `onApiChanged`. It isn't stuck: a private proof takes about 4–7 minutes on an M-series Mac and needs \~4.3 GB of free memory. Only one proof runs at a time. The status shows `proving.phase` and `proving.progress`; the user can close the sheet. LEZ 0.3 includes failed transactions, so the wallet only says `success` when it can prove the effect. Re-read the balance or record you expected to change, and show "not confirmed yet" until then ([Transactions](/docs/concepts/transactions)). Logos Kit's testnet faucet gives each account one drop an hour, each IP five an hour, and 60 an hour in all. The result carries `retryAfterSeconds`; show a countdown. Transactions on the official testnet can take a minute or more to be included. The wallet waits up to about 6 minutes before it reports a send as not included ([Transactions](/docs/concepts/transactions)). SDK amounts are lepta, not LGO (1 LGO = 10^9 lepta). Turn what the user typed into lepta with `kit.sdk.parseUnits(text, 9)`, and show amounts with `kit.sdk.formatUnits(amount, 9)`. Wallets from 0.2.0 recognise only the `v0.3.0` testimonial image, deployed on the official testnet. The preview network's program isn't that image. Post on the testnet ([Networks](/docs/concepts/networks)). The sandbox blocks remote and `data:` images and anything outside your module's folder. Bundle assets in the module and load them with `Qt.resolvedUrl("…")` from inside components. Basecamp runs Qt 6.9's JavaScript engine: `short` is reserved, and there is no `BigInt`, `Intl`, regex lookbehind or named groups. Keep amounts as strings and use `kit.sdk.parseUnits` and `kit.sdk.formatUnits`. Basecamp shows initials for modules installed with `lgpm` during development. Check the icon after a catalog install before changing anything. Still stuck? [Open an issue](https://github.com/logos-kit/logos-kit/issues). # Why Logos Kit (/docs) Logos Kit is two things that ship together: * **A wallet** for the [Logos Execution Zone](https://logos.co) (LEZ): public and private accounts, the native token (LGO) and other tokens, private transfers proved on the user's own machine, and approvals that show what a transaction really does. * **An SDK** your app uses to ask that wallet for things: connect, read balances, propose transfers and program calls, and follow them until they land. It runs in Basecamp apps (QML). A TypeScript client covers Node scripts and tools; a web and React Native connect kit is planned. Keys, accounts and approvals live only in the wallet. Your app asks; the user decides. ## What you get [#what-you-get] | | | | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Private by default** | Public and private accounts side by side. Private transfers are proved locally with RISC Zero: minutes on a laptop, and the app never sees a key. | | **Approvals you can read** | Every request is decoded into effects (who pays whom, which token, which program), with the fee cap and the program's source verification. Calls the wallet can't read are flagged as such. | | **Honest outcomes** | LEZ 0.3 includes failed transactions, so "included" isn't "worked". The wallet proves the effect from chain state and reports `success`, `failure` or `unknown`. | | **Built for Basecamp** | User-facing steps are Basecamp intents, so the shell attests which app is asking. Reads are plain module calls. | | **QML first, TypeScript too** | Apps call the wallet from QML (`LogosKit`). The same client (`@logos-kit/client`, `@logos-kit/codec`, byte-exact LEZ encoding) runs in Node for scripts and tools. Web and React Native connect kits are planned. | | **Apps to copy** | A testimonial app, a faucet app and a template, each with loading, pending, error and unconfirmed states. | ## How the pieces fit [#how-the-pieces-fit] ``` ┌─────────────┐ intent (connect, send, sign, funds) ┌──────────────────┐ │ your app │ ─────────────────────────────────────▶ │ Basecamp shell │ │ (LogosKit) │ └────────┬─────────┘ │ │ ▼ │ │ module call (reads, status) ┌──────────────────┐ │ │ ─────────────────────────────────────▶ │ Logos Kit wallet│ └─────────────┘ │ keys · approvals│ │ proving · policy│ └────────┬─────────┘ ▼ LEZ sequencer (zone) ``` | Piece | Where | | ------------------ | ----------------------------------------------------------------------------------------------------------------- | | Wallet (Basecamp) | `logos_kit_wallet_ui` over the core module `logos_kit_wallet`, from the [Logos Kit catalog](/docs/wallet/install) | | Wallet (terminal) | [`logos-kit` CLI](/docs/wallet/cli), same engine and policy | | QML SDK and UI kit | `sdk/qml/LogosKit/`, `sdk/qml/LogosKitUi/` ([QML SDK](/docs/sdk/qml)) | | TypeScript SDK | `@logos-kit/client`, `@logos-kit/codec` ([client](/docs/sdk/client)) | | Protocol | LWS-0: [methods](/docs/reference/methods) and [intents](/docs/reference/intents) | Logos Kit runs on LEZ testnets and is unaudited. Don't use it with anything of value. The wallet defaults to the official [LEZ testnet](/docs/concepts/networks), which runs LEZ `v0.3.0`. # Capability matrix (/docs/reference/capabilities) {/* Generated by scripts/gen-reference.mjs from docs/protocol/capability-matrix.md. Don't edit. */} Generated by `scripts/capability-matrix.py` (`just capability-matrix`); don't edit by hand. Protocol 0.1.0. **Real** is the Logos Kit wallet (core `logos_kit_wallet` + UI `logos_kit_wallet_ui`); **fake** is the conformance fake (`modules/logos_kit_wallet_fake`). ## Methods (called on the wallet core module) [#methods-called-on-the-wallet-core-module] | Method | User-facing | Through intent | Required params | Real | Fake | | ---------------------------- | ----------- | -------------------------- | ---------------------------- | --------------------------- | --------------------------- | | `lez_connect` | yes | `lez.wallet.connect` | chains | silent restore; else intent | silent restore; else intent | | `lez_disconnect` | — | — | sessionId | yes | yes | | `lez_getSession` | — | — | — | yes | yes | | `lez_getAccounts` | — | — | — | yes | yes | | `lez_chainId` | — | — | — | yes | yes | | `lez_getCapabilities` | — | — | — | yes | yes | | `lez_getBalance` | — | — | chain, account | yes | yes | | `lez_readAccount` | — | — | chain, account, program | yes | yes | | `lez_openExplorer` | — | — | chain | yes | yes | | `lez_signAndSendTransaction` | yes | `lez.transaction.send` | chain, account, instructions | yes | yes | | `lez_getTransactionStatus` | — | — | handle | yes | yes | | `lez_signMessage` | yes | `lez.message.sign` | account, message | intent only | intent only | | `lez_signIn` | yes | `lez.wallet.sign_in` | — | intent only | intent only | | `lez_requestFunds` | yes | `lez.wallet.request_funds` | chain, account | intent only | intent only | | `lez_switchChain` | yes | — | chain | intent only | intent only | ## Intents (opened through the Basecamp shell) [#intents-opened-through-the-basecamp-shell] | Intent | Method | Real | Fake | | -------------------------- | ---------------------------- | ---- | ---- | | `lez.wallet.connect` | `lez_connect` | yes | yes | | `lez.transaction.send` | `lez_signAndSendTransaction` | yes | yes | | `lez.message.sign` | `lez_signMessage` | yes | yes | | `lez.wallet.sign_in` | `lez_signIn` | yes | yes | | `lez.wallet.request_funds` | `lez_requestFunds` | yes | yes | | `lez.wallet.open` | — (opens the wallet) | yes | — | ## Capabilities (`lez_getCapabilities`) [#capabilities-lez_getcapabilities] | Field | Real | Fake | | --------------------- | --------------------------------------------------------- | --------------------------------------------------- | | `accountKinds` | `["public", "private"]` | `["public", "private"]` | | `signTransaction` | `false` | `false` | | `batch` | `{ "maxInstructions": 1, "atomic": "unsupported" }` | `{ "maxInstructions": 1, "atomic": "unsupported" }` | | `proving` | `{ "location": "local", "typicalSeconds": ETA_SHIELD_S }` | `{ "location": "local", "typicalSeconds": 270 }` | | `outcomeVerification` | `["own-account-invariant"]` | `["own-account-invariant"]` | | `signMessage` | `true` | `true` | | `signIn` | `true` | `true` | | `requestFunds` | `true` | `true` | ## Error codes [#error-codes] Whether each wallet can answer with the code (found in its source). | Code | Name | Default message | Real | Fake | | ------ | ------------------------- | --------------------------------------------------------- | ---- | ---- | | 4001 | UserRejected | Request declined | yes | yes | | 4100 | Unauthorized | This app isn't connected to that account | yes | yes | | 4200 | UnsupportedMethod | The wallet doesn't support this request | yes | yes | | 4900 | Disconnected | Wallet disconnected | yes | — | | 4901 | ChainDisconnected | Network disconnected | yes | — | | 4902 | UnknownChain | Unknown network | yes | yes | | -32602 | InvalidParams | | yes | yes | | -32603 | Internal | | yes | yes | | 5700 | UnsupportedCapability | Unsupported capability | yes | — | | 5720 | DuplicateId | Duplicate request id | yes | yes | | 5730 | UnknownHandle | Unknown transaction handle | yes | yes | | 5740 | BatchTooLarge | Too many instructions | yes | yes | | 5760 | AtomicityUnsupported | Atomic execution isn't supported | yes | — | | 6100 | AccountKindUnsupported | This account type can't do that | yes | — | | 6101 | SimulationFailed | Couldn't simulate this transaction | — | — | | 6102 | ProofFailed | Proof failed: nothing was sent | yes | — | | 6103 | SubmissionFailed | The sequencer rejected the transaction | yes | — | | 6104 | InvalidProposal | Invalid transaction proposal | yes | yes | | 6105 | OriginAttestationRequired | Origin could not be verified | — | — | | 6106 | StaleApproval | Something changed since you approved. Please review again | yes | yes | | 6107 | RequestPending | A request is already open in your wallet | yes | — | | 6108 | Timeout | The wallet didn't respond | yes | yes | | 6109 | Unavailable | The wallet isn't available | yes | yes | ## Notifications [#notifications] Basecamp modules can't push LWS-0 notifications to an app; wallets emit `request_updated` and `wallet_event` module events, and apps poll (the SDK does). | Notification | Real | Fake | | ------------------------- | ---- | ---- | | `lez_accountsChanged` | — | — | | `lez_chainChanged` | — | — | | `lez_sessionChanged` | — | — | | `lez_disconnected` | — | — | | `lez_transactionUpdated` | — | — | | `lez_capabilitiesChanged` | — | — | ## Fake wallet scenarios [#fake-wallet-scenarios] | Scenario | What the fake does | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `happy` | Connect shares one public account (and a private handle if asked). Balance 1000000000. A send answers a handle; status reads go submitted → included/success. The faucet funds 1000000000. | | `reject` | The user declines every wallet prompt: each intent answers `cancelled` (LWS-0 4001). | | `timeout` | Like happy, but the send intent answers only after 50 s: the SDK times out at 45 s (6108) and the handle arrives late (`onLateResult`). | | `stale` | The send is approved, then the chain moved: status goes signing → dropped with error 6106 (StaleApproval). Nothing was sent. | | `unknown_outcome` | The send is included but the wallet can't tell whether it worked: included, outcome `unknown`, outcomeSource `none`. | | `failed_onchain` | The send is included and failed on chain: included, outcome `failure` (own-account-invariant). | | `rate_limited` | Balance 0. The faucet refuses: `rate_limited`, retryAfterSeconds 60. Everything else is happy. | | `faucet_unknown` | Balance 0. The faucet answer is lost: `outcome_unknown`. The funds do arrive (balance +1000000000), so a dApp that re-reads the balance finds them. | | `no_funds` | Balance 0. A send is refused by the wallet (intent answers `failed`, as in every scenario while the account is empty); the faucet funds 1000000000, after which sends work. | | `network_switch` | After the first connect, the user switches the wallet to lez:local: the next lez\_chainId answers lez:local, the old session is gone and calls naming the old chain get 4902. | | `unavailable` | The wallet is not running: every method answers 6109, every intent `unavailable`. | # Errors (/docs/reference/errors) Errors are `{ code, message, data? }`. Codes follow EIP-1193 and EIP-5792 where those exist, so habits carry over. **A user saying no is not an error.** Check `kit.sdk.isUserRejection(e)` and show nothing (or "Cancelled"). | Code | Name | What to show | | ------ | ---------------------- | ------------------------------------------------------------------ | | 4001 | UserRejected | nothing | | 4100 | Unauthorized | "Connect again": the account isn't shared with this app (any more) | | 4200 | UnsupportedMethod | this wallet doesn't do that | | 4900 | Disconnected | the wallet is locked or not set up | | 4901 | ChainDisconnected | can't reach the network; retry later | | 4902 | UnknownChain | the wallet is on another network | | -32602 | InvalidParams | a bug in your call | | -32603 | Internal | the wallet couldn't complete it (the user saw why) | | 5720 | DuplicateId | this proposal id was used; `data` has the first handle | | 5730 | UnknownHandle | not your handle, or expired | | 6100 | AccountKindUnsupported | e.g. a private account where only public works | | 6102 | ProofFailed | proving failed; nothing was sent | | 6103 | SubmissionFailed | the network refused it | | 6104 | InvalidProposal | the wallet can't build or decode your proposal | | 6106 | StaleApproval | chain state moved since approval; propose again | | 6107 | RequestPending | a wallet sheet is already open for your app | | 6108 | Timeout | no answer yet: the wallet may still send it | | 6109 | Unavailable | no wallet, or no faucet on this network | Through Basecamp intents the shell carries only six codes (`cancelled`, `bad_request`, `failed`, `timeout`, `unavailable`, `not_declared`); the SDK maps them to the table above. `not_declared` means your `metadata.json` `uses` doesn't list the intent. # Basecamp intents (/docs/reference/intents) Wallet capabilities for Basecamp apps, as [app-to-app intents](https://github.com/logos-co/logos-basecamp/blob/master/docs/app-to-app-intents.md). Any wallet may provide them; Logos Kit's `logos_kit_wallet_ui` is the reference provider. Machine-readable: `protocol/schema/intents.json` (the `provides` block is generated from it). The payload types are LWS-0's (`protocol/schema/lws0.schema.json`). ## Declaring use [#declaring-use] ```json "uses": [ { "intent": "lez.wallet.connect" }, { "intent": "lez.transaction.send" } ] ``` Entries must be **objects**: a bare string array parses, declares nothing, and every request fails `not_declared`. ## Intents [#intents] | Intent | Params (shell-enforced) | Answers `data` | Answered when | | -------------------------- | -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | ---------------------------------------------------------------------- | | `lez.wallet.connect` | `chains: array` (req.), `accountKinds?: array`, `capabilities?: array`, `signIn?: object` | LWS-0 `Session` (+ `signIn` result if asked) | the user connects | | `lez.transaction.send` | `chain: string`, `account: string`, `instructions: array` (req.), `id?: string`, `atomicRequired?: bool` | `{ handle, txHash? }` | the user **approves** (proving and inclusion continue; read by handle) | | `lez.message.sign` | `account: string`, `message: string` (base64) | `{ signature, publicKey, tag: "LEZ/message/v1" }` | the user signs | | `lez.wallet.sign_in` | `domain, uri, nonce, issuedAt: string` (+ optional SIWE fields) | `{ account, signedMessage, signature }` | the user signs | | `lez.wallet.request_funds` | `chain: string`, `account: string` | `{ status, amount?, txHash?, retryAfterSeconds?, shieldHandle? }` | the faucet answered | | `lez.wallet.open` | none (`handoff: true`, `web: true`) | `{}` | at once | ### Semantics [#semantics] * **Requester identity** is the shell-attested `requesterName` (the app's module name). Grants are keyed by it, so a grant made through `lez.wallet.connect` also covers that app's direct module calls. * **One request at a time.** A wallet with a request open answers a second with `failed`. * **Answer at acceptance.** A private transaction proves for minutes; the shell's backstop is 10 minutes. `lez.transaction.send` answers with a **handle** once approved; the app follows it with `lez_getTransactionStatus` (a read on the wallet's core module). * **Duplicate ids.** `id` is unique per (app, account). A repeat is refused (the wallet shows why; the intent answers `failed`; a direct module call gets `5720` with the first handle in `data`). * **Transactions are public calls.** Apps propose one instruction from a public account. Private transfers start in the wallet itself; `request_funds` into a private account funds a public one, then queues the shield for approval (`shieldHandle`). * **Signatures** are BIP-340 over a tagged hash (`SHA256(SHA256(tag)‖SHA256(tag)‖msg)`), tags `LEZ/message/v1` and `LEZ/signin/v1`, so they can never be transaction signatures. * Sign-in text is EIP-4361-shaped (` wants you to sign in with your LEZ account:` …, `Chain ID: lez:`); single-line fields only. ### Errors [#errors] The shell carries only its six codes. What a requester sees, and what it means here: | `res.error` | Meaning | `@logos-kit/client` code | | -------------- | ------------------------------------------------------------------------------------- | ------------------------ | | `cancelled` | the user declined or closed the sheet | 4001 | | `bad_request` | params the wallet can't serve (wrong chain, not a wallet account, malformed proposal) | -32602 | | `failed` | the wallet couldn't complete it (the user saw why) | -32603 | | `timeout` | no answer in 10 minutes | 6108 | | `unavailable` | no wallet installed, or the app may not ask | 6109 | | `not_declared` | the app's `uses` doesn't list the intent | -32603 | ## Reads (no intent) [#reads-no-intent] Status, balances, sessions and chain state are module calls on `logos_kit_wallet` (`logos.callModuleAsync`), answering `{"value": …}` or `{"error": {code, message, data?}}`: `lez_getSession`, `lez_getAccounts`, `lez_getBalance`, `lez_readAccount`, `lez_getTransactionStatus`, `lez_getCapabilities`, `lez_disconnect`. A module may also propose directly with `lez_signAndSendTransaction` (it needs a grant; the wallet shows the request next time it is open). ## Upstream proposal (draft) [#upstream-proposal-draft] Basecamp has no registry of well-known intents yet. We propose registering `lez.*` as the wallet namespace for the Logos Execution Zone, with the table above as its first version, so any wallet can provide it and any app can depend on it without knowing which wallet is installed. # Wallet methods (LWS-0) (/docs/reference/methods) {/* Generated by scripts/gen-reference.mjs from protocol/schema. Do not edit. */} Protocol version **0.1.0**. Reads are module calls on `logos_kit_wallet` (`logos.callModuleAsync`); user-facing steps open the wallet through a Basecamp intent. The QML and TypeScript SDKs wrap all of them: you rarely call these by name. ## `lez_connect` [#lez_connect] LWS-0 connect (silent restore only; use the lez.wallet.connect intent). **User-facing.** Open it with the `lez.wallet.connect` intent; the wallet shows its own sheet. ### Params [#params] | Name | Type | Required | Notes | | -------------- | ---------------------------------------------------------------------------------------------------------- | -------- | ----- | | `chains` | string\[] | yes | | | `accountKinds` | "public" \| "private"\[] | no | | | `capabilities` | "accounts" \| "read\_public" \| "read\_private" \| "propose\_tx" \| "sign\_message" \| "request\_proof"\[] | no | | | `silent` | boolean | no | | | `signIn` | object | no | | ### Result [#result] `object`: `sessionId`, `origin`, `chains`, `accounts`, `expiry`, `capabilities`, `signIn`
JSON Schema ```json { "params": { "type": "object", "required": [ "chains" ], "properties": { "chains": { "type": "array", "items": { "type": "string", "pattern": "^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$", "description": "CAIP-2 chain id, e.g. \"lez:testnet\"" }, "minItems": 1 }, "accountKinds": { "type": "array", "items": { "anyOf": [ { "type": "string", "const": "public" }, { "type": "string", "const": "private" } ] } }, "capabilities": { "type": "array", "items": { "anyOf": [ { "type": "string", "const": "accounts" }, { "type": "string", "const": "read_public" }, { "type": "string", "const": "read_private" }, { "type": "string", "const": "propose_tx" }, { "type": "string", "const": "sign_message" }, { "type": "string", "const": "request_proof" } ] } }, "silent": { "type": "boolean" }, "signIn": { "type": "object", "required": [ "domain", "uri", "nonce", "issuedAt" ], "properties": { "domain": { "type": "string", "maxLength": 253 }, "statement": { "type": "string", "maxLength": 512 }, "uri": { "type": "string", "maxLength": 2048 }, "nonce": { "type": "string", "minLength": 8, "maxLength": 64 }, "issuedAt": { "type": "string", "format": "date-time" }, "expirationTime": { "type": "string", "format": "date-time" }, "notBefore": { "type": "string", "format": "date-time" }, "requestId": { "type": "string", "maxLength": 64 } } } } }, "result": { "type": "object", "required": [ "sessionId", "origin", "chains", "accounts", "capabilities" ], "properties": { "sessionId": { "type": "string", "minLength": 16, "maxLength": 128 }, "origin": { "type": "string" }, "chains": { "type": "array", "items": { "type": "string", "pattern": "^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$", "description": "CAIP-2 chain id, e.g. \"lez:testnet\"" } }, "accounts": { "type": "array", "items": { "type": "object", "required": [ "address", "kind", "chain", "capabilities" ], "properties": { "address": { "anyOf": [ { "type": "string", "pattern": "^[1-9A-HJ-NP-Za-km-z]{32,44}$", "description": "Base58-encoded 32-byte LEZ account id" }, { "type": "string", "pattern": "^pvt_[A-Za-z0-9_-]{16,64}$", "description": "Opaque per-origin private account handle" } ] }, "kind": { "anyOf": [ { "type": "string", "const": "public" }, { "type": "string", "const": "private" } ] }, "chain": { "type": "string", "pattern": "^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$", "description": "CAIP-2 chain id, e.g. \"lez:testnet\"" }, "publicKey": { "type": "string", "pattern": "^[A-Za-z0-9+/]*={0,2}$", "maxLength": 65000 }, "label": { "type": "string", "maxLength": 64 }, "capabilities": { "type": "array", "items": { "anyOf": [ { "type": "string", "const": "accounts" }, { "type": "string", "const": "read_public" }, { "type": "string", "const": "read_private" }, { "type": "string", "const": "propose_tx" }, { "type": "string", "const": "sign_message" }, { "type": "string", "const": "request_proof" } ] } } } } }, "expiry": { "type": "string", "format": "date-time" }, "capabilities": { "type": "object", "required": [ "accountKinds", "signTransaction", "batch", "outcomeVerification", "signMessage", "signIn", "requestFunds" ], "properties": { "accountKinds": { "type": "array", "items": { "anyOf": [ { "type": "string", "const": "public" }, { "type": "string", "const": "private" } ] } }, "signTransaction": { "type": "boolean" }, "batch": { "type": "object", "required": [ "maxInstructions", "atomic" ], "properties": { "maxInstructions": { "type": "integer", "minimum": 1, "maximum": 16 }, "atomic": { "anyOf": [ { "type": "string", "const": "supported" }, { "type": "string", "const": "unsupported" } ] } } }, "proving": { "type": "object", "required": [ "location" ], "properties": { "location": { "anyOf": [ { "type": "string", "const": "local" }, { "type": "string", "const": "paired" }, { "type": "string", "const": "user-owned" }, { "type": "string", "const": "tee" } ] }, "typicalSeconds": { "type": "integer", "minimum": 0 } } }, "outcomeVerification": { "type": "array", "items": { "anyOf": [ { "type": "string", "const": "own-account-invariant" }, { "type": "string", "const": "program-event" }, { "type": "string", "const": "node" } ] } }, "signMessage": { "type": "boolean" }, "signIn": { "type": "boolean" }, "requestFunds": { "type": "boolean" } } }, "signIn": { "type": "object", "required": [ "account", "signedMessage", "signature" ], "properties": { "account": { "type": "object", "required": [ "address", "kind", "chain", "capabilities" ], "properties": { "address": { "anyOf": [ { "type": "string", "pattern": "^[1-9A-HJ-NP-Za-km-z]{32,44}$", "description": "Base58-encoded 32-byte LEZ account id" }, { "type": "string", "pattern": "^pvt_[A-Za-z0-9_-]{16,64}$", "description": "Opaque per-origin private account handle" } ] }, "kind": { "anyOf": [ { "type": "string", "const": "public" }, { "type": "string", "const": "private" } ] }, "chain": { "type": "string", "pattern": "^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$", "description": "CAIP-2 chain id, e.g. \"lez:testnet\"" }, "publicKey": { "type": "string", "pattern": "^[A-Za-z0-9+/]*={0,2}$", "maxLength": 65000 }, "label": { "type": "string", "maxLength": 64 }, "capabilities": { "type": "array", "items": { "anyOf": [ { "type": "string", "const": "accounts" }, { "type": "string", "const": "read_public" }, { "type": "string", "const": "read_private" }, { "type": "string", "const": "propose_tx" }, { "type": "string", "const": "sign_message" }, { "type": "string", "const": "request_proof" } ] } } } }, "signedMessage": { "type": "string" }, "signature": { "type": "string", "pattern": "^[A-Za-z0-9+/]*={0,2}$", "maxLength": 65000 } } } } } } ```
## `lez_disconnect` [#lez_disconnect] LWS-0: drop this app's grants. **Read.** A module call; no prompt. ### Params [#params-1] | Name | Type | Required | Notes | | ----------- | ------ | -------- | ----- | | `sessionId` | string | yes | | ### Result [#result-1] `null`
JSON Schema ```json { "params": { "type": "object", "required": [ "sessionId" ], "properties": { "sessionId": { "type": "string" } } }, "result": { "type": "null" } } ```
## `lez_getSession` [#lez_getsession] LWS-0: this app's session, or null. **Read.** A module call; no prompt. ### Params [#params-2] | Name | Type | Required | Notes | | ----------- | ------ | -------- | ----- | | `sessionId` | string | no | | ### Result [#result-2] `object \| null`
JSON Schema ```json { "params": { "type": "object", "properties": { "sessionId": { "type": "string" } } }, "result": { "anyOf": [ { "type": "object", "required": [ "sessionId", "origin", "chains", "accounts", "capabilities" ], "properties": { "sessionId": { "type": "string", "minLength": 16, "maxLength": 128 }, "origin": { "type": "string" }, "chains": { "type": "array", "items": { "type": "string", "pattern": "^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$", "description": "CAIP-2 chain id, e.g. \"lez:testnet\"" } }, "accounts": { "type": "array", "items": { "type": "object", "required": [ "address", "kind", "chain", "capabilities" ], "properties": { "address": { "anyOf": [ { "type": "string", "pattern": "^[1-9A-HJ-NP-Za-km-z]{32,44}$", "description": "Base58-encoded 32-byte LEZ account id" }, { "type": "string", "pattern": "^pvt_[A-Za-z0-9_-]{16,64}$", "description": "Opaque per-origin private account handle" } ] }, "kind": { "anyOf": [ { "type": "string", "const": "public" }, { "type": "string", "const": "private" } ] }, "chain": { "type": "string", "pattern": "^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$", "description": "CAIP-2 chain id, e.g. \"lez:testnet\"" }, "publicKey": { "type": "string", "pattern": "^[A-Za-z0-9+/]*={0,2}$", "maxLength": 65000 }, "label": { "type": "string", "maxLength": 64 }, "capabilities": { "type": "array", "items": { "anyOf": [ { "type": "string", "const": "accounts" }, { "type": "string", "const": "read_public" }, { "type": "string", "const": "read_private" }, { "type": "string", "const": "propose_tx" }, { "type": "string", "const": "sign_message" }, { "type": "string", "const": "request_proof" } ] } } } } }, "expiry": { "type": "string", "format": "date-time" }, "capabilities": { "type": "object", "required": [ "accountKinds", "signTransaction", "batch", "outcomeVerification", "signMessage", "signIn", "requestFunds" ], "properties": { "accountKinds": { "type": "array", "items": { "anyOf": [ { "type": "string", "const": "public" }, { "type": "string", "const": "private" } ] } }, "signTransaction": { "type": "boolean" }, "batch": { "type": "object", "required": [ "maxInstructions", "atomic" ], "properties": { "maxInstructions": { "type": "integer", "minimum": 1, "maximum": 16 }, "atomic": { "anyOf": [ { "type": "string", "const": "supported" }, { "type": "string", "const": "unsupported" } ] } } }, "proving": { "type": "object", "required": [ "location" ], "properties": { "location": { "anyOf": [ { "type": "string", "const": "local" }, { "type": "string", "const": "paired" }, { "type": "string", "const": "user-owned" }, { "type": "string", "const": "tee" } ] }, "typicalSeconds": { "type": "integer", "minimum": 0 } } }, "outcomeVerification": { "type": "array", "items": { "anyOf": [ { "type": "string", "const": "own-account-invariant" }, { "type": "string", "const": "program-event" }, { "type": "string", "const": "node" } ] } }, "signMessage": { "type": "boolean" }, "signIn": { "type": "boolean" }, "requestFunds": { "type": "boolean" } } }, "signIn": { "type": "object", "required": [ "account", "signedMessage", "signature" ], "properties": { "account": { "type": "object", "required": [ "address", "kind", "chain", "capabilities" ], "properties": { "address": { "anyOf": [ { "type": "string", "pattern": "^[1-9A-HJ-NP-Za-km-z]{32,44}$", "description": "Base58-encoded 32-byte LEZ account id" }, { "type": "string", "pattern": "^pvt_[A-Za-z0-9_-]{16,64}$", "description": "Opaque per-origin private account handle" } ] }, "kind": { "anyOf": [ { "type": "string", "const": "public" }, { "type": "string", "const": "private" } ] }, "chain": { "type": "string", "pattern": "^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$", "description": "CAIP-2 chain id, e.g. \"lez:testnet\"" }, "publicKey": { "type": "string", "pattern": "^[A-Za-z0-9+/]*={0,2}$", "maxLength": 65000 }, "label": { "type": "string", "maxLength": 64 }, "capabilities": { "type": "array", "items": { "anyOf": [ { "type": "string", "const": "accounts" }, { "type": "string", "const": "read_public" }, { "type": "string", "const": "read_private" }, { "type": "string", "const": "propose_tx" }, { "type": "string", "const": "sign_message" }, { "type": "string", "const": "request_proof" } ] } } } }, "signedMessage": { "type": "string" }, "signature": { "type": "string", "pattern": "^[A-Za-z0-9+/]*={0,2}$", "maxLength": 65000 } } } } }, { "type": "null" } ] } } ```
## `lez_getAccounts` [#lez_getaccounts] LWS-0: accounts shared with this app. **Read.** A module call; no prompt. ### Params [#params-3] *No parameters.* ### Result [#result-3] `object[]`
JSON Schema ```json { "params": { "type": "object", "properties": {} }, "result": { "type": "array", "items": { "type": "object", "required": [ "address", "kind", "chain", "capabilities" ], "properties": { "address": { "anyOf": [ { "type": "string", "pattern": "^[1-9A-HJ-NP-Za-km-z]{32,44}$", "description": "Base58-encoded 32-byte LEZ account id" }, { "type": "string", "pattern": "^pvt_[A-Za-z0-9_-]{16,64}$", "description": "Opaque per-origin private account handle" } ] }, "kind": { "anyOf": [ { "type": "string", "const": "public" }, { "type": "string", "const": "private" } ] }, "chain": { "type": "string", "pattern": "^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$", "description": "CAIP-2 chain id, e.g. \"lez:testnet\"" }, "publicKey": { "type": "string", "pattern": "^[A-Za-z0-9+/]*={0,2}$", "maxLength": 65000 }, "label": { "type": "string", "maxLength": 64 }, "capabilities": { "type": "array", "items": { "anyOf": [ { "type": "string", "const": "accounts" }, { "type": "string", "const": "read_public" }, { "type": "string", "const": "read_private" }, { "type": "string", "const": "propose_tx" }, { "type": "string", "const": "sign_message" }, { "type": "string", "const": "request_proof" } ] } } } } } } ```
## `lez_chainId` [#lez_chainid] LWS-0: the network the wallet is on (\{chain}). **Read.** A module call; no prompt. ### Behaviour [#behaviour] * Answers any caller, with or without a session: the network is public metadata, like `eth_chainId`. It never reveals accounts or balances. * The value changes when the user switches networks in the wallet. The QML SDK checks it on start, when your view is shown, and every 5 s while visible, and rebuilds `kit.api` when it changes (set `followWallet: false` to pin a network). ### Params [#params-4] *No parameters.* ### Result [#result-4] `object`: `chain`
JSON Schema ```json { "params": { "type": "object", "properties": {} }, "result": { "type": "object", "required": [ "chain" ], "properties": { "chain": { "type": "string", "pattern": "^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$", "description": "CAIP-2 chain id, e.g. \"lez:testnet\"" } } } } ```
## `lez_getCapabilities` [#lez_getcapabilities] LWS-0: what this wallet supports. **Read.** A module call; no prompt. ### Params [#params-5] | Name | Type | Required | Notes | | ------- | ------ | -------- | ----------------------------------- | | `chain` | string | no | CAIP-2 chain id, e.g. "lez:testnet" | ### Result [#result-5] `object`: `accountKinds`, `signTransaction`, `batch`, `proving`, `outcomeVerification`, `signMessage`, `signIn`, `requestFunds`
JSON Schema ```json { "params": { "type": "object", "properties": { "chain": { "type": "string", "pattern": "^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$", "description": "CAIP-2 chain id, e.g. \"lez:testnet\"" } } }, "result": { "type": "object", "required": [ "accountKinds", "signTransaction", "batch", "outcomeVerification", "signMessage", "signIn", "requestFunds" ], "properties": { "accountKinds": { "type": "array", "items": { "anyOf": [ { "type": "string", "const": "public" }, { "type": "string", "const": "private" } ] } }, "signTransaction": { "type": "boolean" }, "batch": { "type": "object", "required": [ "maxInstructions", "atomic" ], "properties": { "maxInstructions": { "type": "integer", "minimum": 1, "maximum": 16 }, "atomic": { "anyOf": [ { "type": "string", "const": "supported" }, { "type": "string", "const": "unsupported" } ] } } }, "proving": { "type": "object", "required": [ "location" ], "properties": { "location": { "anyOf": [ { "type": "string", "const": "local" }, { "type": "string", "const": "paired" }, { "type": "string", "const": "user-owned" }, { "type": "string", "const": "tee" } ] }, "typicalSeconds": { "type": "integer", "minimum": 0 } } }, "outcomeVerification": { "type": "array", "items": { "anyOf": [ { "type": "string", "const": "own-account-invariant" }, { "type": "string", "const": "program-event" }, { "type": "string", "const": "node" } ] } }, "signMessage": { "type": "boolean" }, "signIn": { "type": "boolean" }, "requestFunds": { "type": "boolean" } } } } ```
## `lez_getBalance` [#lez_getbalance] LWS-0: balance of a shared account. **Read.** A module call; no prompt. ### Params [#params-6] | Name | Type | Required | Notes | | --------- | ---------------- | -------- | ------------------------------------- | | `chain` | string | yes | CAIP-2 chain id, e.g. "lez:testnet" | | `account` | string \| string | yes | | | `asset` | string | no | Base58-encoded 32-byte LEZ account id | ### Result [#result-6] `object`: `asset`, `amount`, `synced`, `asOfBlock`
JSON Schema ```json { "params": { "type": "object", "required": [ "chain", "account" ], "properties": { "chain": { "type": "string", "pattern": "^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$", "description": "CAIP-2 chain id, e.g. \"lez:testnet\"" }, "account": { "anyOf": [ { "type": "string", "pattern": "^[1-9A-HJ-NP-Za-km-z]{32,44}$", "description": "Base58-encoded 32-byte LEZ account id" }, { "type": "string", "pattern": "^pvt_[A-Za-z0-9_-]{16,64}$", "description": "Opaque per-origin private account handle" } ] }, "asset": { "type": "string", "pattern": "^[1-9A-HJ-NP-Za-km-z]{32,44}$", "description": "Base58-encoded 32-byte LEZ account id" } } }, "result": { "type": "object", "required": [ "asset", "amount", "synced" ], "properties": { "asset": { "anyOf": [ { "type": "string", "const": "native" }, { "type": "string", "pattern": "^[1-9A-HJ-NP-Za-km-z]{32,44}$", "description": "Base58-encoded 32-byte LEZ account id" } ] }, "amount": { "type": "string", "pattern": "^(0|[1-9][0-9]{0,38})$", "description": "u128 as a decimal string. The pattern allows up to 39 digits; values above 2^128-1 are rejected by the codec and wallet with -32602." }, "synced": { "type": "boolean" }, "asOfBlock": { "type": "string", "pattern": "^[0-9]+$" } } } } ```
## `lez_readAccount` [#lez_readaccount] LWS-0: a public account's data for one program (chain state). **Read.** A module call; no prompt. ### Params [#params-7] | Name | Type | Required | Notes | | --------- | ------ | -------- | ------------------------------------- | | `chain` | string | yes | CAIP-2 chain id, e.g. "lez:testnet" | | `account` | string | yes | Base58-encoded 32-byte LEZ account id | | `program` | string | yes | Base58-encoded 32-byte LEZ account id | ### Result [#result-7] `object`: `nonce`, `data`
JSON Schema ```json { "params": { "type": "object", "required": [ "chain", "account", "program" ], "properties": { "chain": { "type": "string", "pattern": "^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$", "description": "CAIP-2 chain id, e.g. \"lez:testnet\"" }, "account": { "type": "string", "pattern": "^[1-9A-HJ-NP-Za-km-z]{32,44}$", "description": "Base58-encoded 32-byte LEZ account id" }, "program": { "type": "string", "pattern": "^[1-9A-HJ-NP-Za-km-z]{32,44}$", "description": "Base58-encoded 32-byte LEZ account id" } } }, "result": { "type": "object", "required": [ "nonce", "data" ], "properties": { "nonce": { "type": "string", "pattern": "^(0|[1-9][0-9]{0,38})$", "description": "u128 as a decimal string. The pattern allows up to 39 digits; values above 2^128-1 are rejected by the codec and wallet with -32602." }, "data": { "type": "string", "pattern": "^[A-Za-z0-9+/]*={0,2}$", "maxLength": 65000 } } } } ```
## `lez_openExplorer` [#lez_openexplorer] LWS-0: open the zone's explorer at a transaction or account (the wallet builds the URL). **Read.** A module call; no prompt. ### Behaviour [#behaviour-1] * Opens the page in the user's browser (an OS side effect), because the Basecamp QML sandbox can't open links itself. The result's `url` is the page it opened. * The URL is built by the wallet from a fixed base. Only `lez:testnet` has an explorer (`https://explorer.testnet.lez.logos.co`); other networks answer `5700`. A `chain` other than the wallet's current one answers `4902`. * Pass **exactly one** of `txHash` (32 bytes of hex, optional `0x`) or `account` (a valid account id); anything else is `InvalidParams`. * Rate-limited: at most one page per second across all apps (`6107` otherwise). * Only apps with a connected session can call it. ### Params [#params-8] | Name | Type | Required | Notes | | --------- | ------ | -------- | ------------------------------------- | | `chain` | string | yes | CAIP-2 chain id, e.g. "lez:testnet" | | `txHash` | string | no | | | `account` | string | no | Base58-encoded 32-byte LEZ account id | ### Result [#result-8] `object`: `url`
JSON Schema ```json { "params": { "type": "object", "required": [ "chain" ], "properties": { "chain": { "type": "string", "pattern": "^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$", "description": "CAIP-2 chain id, e.g. \"lez:testnet\"" }, "txHash": { "type": "string", "pattern": "^0x[0-9a-f]{64}$" }, "account": { "type": "string", "pattern": "^[1-9A-HJ-NP-Za-km-z]{32,44}$", "description": "Base58-encoded 32-byte LEZ account id" } } }, "result": { "type": "object", "required": [ "url" ], "properties": { "url": { "type": "string" } } } } ```
## `lez_signAndSendTransaction` [#lez_signandsendtransaction] LWS-0: propose a transaction for the user's approval; answers a handle. **User-facing.** Open it with the `lez.transaction.send` intent; the wallet shows its own sheet. ### Params [#params-9] | Name | Type | Required | Notes | | ---------------- | ---------------- | -------- | ----------------------------------- | | `chain` | string | yes | CAIP-2 chain id, e.g. "lez:testnet" | | `account` | string \| string | yes | | | `id` | string | no | | | `instructions` | object\[] | yes | | | `atomicRequired` | boolean | no | | ### Result [#result-9] `object`: `handle`, `txHash`
JSON Schema ```json { "params": { "type": "object", "required": [ "chain", "account", "instructions" ], "properties": { "chain": { "type": "string", "pattern": "^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$", "description": "CAIP-2 chain id, e.g. \"lez:testnet\"" }, "account": { "anyOf": [ { "type": "string", "pattern": "^[1-9A-HJ-NP-Za-km-z]{32,44}$", "description": "Base58-encoded 32-byte LEZ account id" }, { "type": "string", "pattern": "^pvt_[A-Za-z0-9_-]{16,64}$", "description": "Opaque per-origin private account handle" } ] }, "id": { "type": "string", "minLength": 1, "maxLength": 64 }, "instructions": { "type": "array", "items": { "type": "object", "required": [ "program", "accounts", "data" ], "properties": { "program": { "type": "string", "pattern": "^[1-9A-HJ-NP-Za-km-z]{32,44}$", "description": "Base58-encoded 32-byte LEZ account id" }, "accounts": { "type": "array", "items": { "type": "object", "required": [ "account", "writable", "signer" ], "properties": { "account": { "anyOf": [ { "type": "string", "pattern": "^[1-9A-HJ-NP-Za-km-z]{32,44}$", "description": "Base58-encoded 32-byte LEZ account id" }, { "type": "string", "pattern": "^pvt_[A-Za-z0-9_-]{16,64}$", "description": "Opaque per-origin private account handle" } ] }, "writable": { "type": "boolean" }, "signer": { "type": "boolean" } } }, "maxItems": 64 }, "data": { "type": "string", "pattern": "^[A-Za-z0-9+/]*={0,2}$", "maxLength": 65000 }, "display": { "type": "object", "properties": { "method": { "type": "string", "maxLength": 64 }, "args": { "type": "object", "patternProperties": { "^.*$": {} } } } } } }, "minItems": 1, "maxItems": 16 }, "atomicRequired": { "type": "boolean" } } }, "result": { "type": "object", "required": [ "handle" ], "properties": { "handle": { "type": "string", "minLength": 16, "maxLength": 128 }, "txHash": { "type": "string", "pattern": "^0x[0-9a-f]{64}$" } } } } ```
## `lez_getTransactionStatus` [#lez_gettransactionstatus] LWS-0: status of this app's request. **Read.** A module call; no prompt. ### Params [#params-10] | Name | Type | Required | Notes | | -------- | ------ | -------- | ----- | | `handle` | string | yes | | ### Result [#result-10] `object`: `handle`, `chain`, `txHash`, `lifecycle`, `outcome`, `outcomeSource`, `block`, `expectedEffects`, `observedEffects`, `fee`, `proving`, `error`
JSON Schema ```json { "params": { "type": "object", "required": [ "handle" ], "properties": { "handle": { "type": "string" } } }, "result": { "type": "object", "required": [ "handle", "chain", "lifecycle", "outcome", "outcomeSource" ], "properties": { "handle": { "type": "string" }, "chain": { "type": "string", "pattern": "^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$", "description": "CAIP-2 chain id, e.g. \"lez:testnet\"" }, "txHash": { "type": "string", "pattern": "^0x[0-9a-f]{64}$" }, "lifecycle": { "anyOf": [ { "type": "string", "const": "awaiting_approval" }, { "type": "string", "const": "building" }, { "type": "string", "const": "proving" }, { "type": "string", "const": "signing" }, { "type": "string", "const": "submitted" }, { "type": "string", "const": "included" }, { "type": "string", "const": "finalized" }, { "type": "string", "const": "rejected" }, { "type": "string", "const": "dropped" }, { "type": "string", "const": "expired" } ] }, "outcome": { "anyOf": [ { "type": "string", "const": "success" }, { "type": "string", "const": "failure" }, { "type": "string", "const": "unknown" } ] }, "outcomeSource": { "anyOf": [ { "type": "string", "const": "own-account-invariant" }, { "type": "string", "const": "program-event" }, { "type": "string", "const": "node" }, { "type": "string", "const": "none" } ] }, "block": { "type": "object", "required": [ "id" ], "properties": { "id": { "type": "string", "pattern": "^[0-9]+$" }, "timestamp": { "type": "string", "format": "date-time" } } }, "expectedEffects": { "type": "array", "items": { "type": "object", "required": [ "account", "field" ], "properties": { "account": { "anyOf": [ { "type": "string", "pattern": "^[1-9A-HJ-NP-Za-km-z]{32,44}$", "description": "Base58-encoded 32-byte LEZ account id" }, { "type": "string", "pattern": "^pvt_[A-Za-z0-9_-]{16,64}$", "description": "Opaque per-origin private account handle" } ] }, "field": { "anyOf": [ { "type": "string", "const": "balance" }, { "type": "string", "const": "token_balance" }, { "type": "string", "const": "data" }, { "type": "string", "const": "authority" }, { "type": "string", "const": "nonce" } ] }, "asset": { "type": "string" }, "before": { "type": "string" }, "after": { "type": "string" } } } }, "observedEffects": { "type": "array", "items": { "type": "object", "required": [ "account", "field" ], "properties": { "account": { "anyOf": [ { "type": "string", "pattern": "^[1-9A-HJ-NP-Za-km-z]{32,44}$", "description": "Base58-encoded 32-byte LEZ account id" }, { "type": "string", "pattern": "^pvt_[A-Za-z0-9_-]{16,64}$", "description": "Opaque per-origin private account handle" } ] }, "field": { "anyOf": [ { "type": "string", "const": "balance" }, { "type": "string", "const": "token_balance" }, { "type": "string", "const": "data" }, { "type": "string", "const": "authority" }, { "type": "string", "const": "nonce" } ] }, "asset": { "type": "string" }, "before": { "type": "string" }, "after": { "type": "string" } } } }, "fee": { "type": "object", "properties": { "estimatedMax": { "type": "string", "pattern": "^(0|[1-9][0-9]{0,38})$", "description": "u128 as a decimal string. The pattern allows up to 39 digits; values above 2^128-1 are rejected by the codec and wallet with -32602." }, "used": { "type": "string", "pattern": "^(0|[1-9][0-9]{0,38})$", "description": "u128 as a decimal string. The pattern allows up to 39 digits; values above 2^128-1 are rejected by the codec and wallet with -32602." }, "gasLimit": { "type": "string", "pattern": "^[0-9]+$" } } }, "proving": { "type": "object", "required": [ "phase" ], "properties": { "phase": { "anyOf": [ { "type": "string", "const": "preparing" }, { "type": "string", "const": "proving" }, { "type": "string", "const": "submitting" }, { "type": "string", "const": "waiting_for_block" } ] }, "progress": { "type": "number", "minimum": 0, "maximum": 100 }, "etaSeconds": { "type": "integer", "minimum": 0 }, "startedAt": { "type": "string", "format": "date-time" } } }, "error": { "type": "object", "required": [ "code", "message" ], "properties": { "code": { "type": "integer" }, "message": { "type": "string" }, "data": {} } } } } } ```
## `lez_signMessage` [#lez_signmessage] LWS-0 (needs the lez.message.sign intent). **User-facing.** Open it with the `lez.message.sign` intent; the wallet shows its own sheet. ### Params [#params-11] | Name | Type | Required | Notes | | --------- | ------ | -------- | ------------------------------------- | | `account` | string | yes | Base58-encoded 32-byte LEZ account id | | `message` | string | yes | | ### Result [#result-11] `object`: `signature`, `publicKey`, `tag`
JSON Schema ```json { "params": { "type": "object", "required": [ "account", "message" ], "properties": { "account": { "type": "string", "pattern": "^[1-9A-HJ-NP-Za-km-z]{32,44}$", "description": "Base58-encoded 32-byte LEZ account id" }, "message": { "type": "string", "pattern": "^[A-Za-z0-9+/]*={0,2}$", "maxLength": 65000 } } }, "result": { "type": "object", "required": [ "signature", "publicKey", "tag" ], "properties": { "signature": { "type": "string", "pattern": "^[A-Za-z0-9+/]*={0,2}$", "maxLength": 65000 }, "publicKey": { "type": "string", "pattern": "^[A-Za-z0-9+/]*={0,2}$", "maxLength": 65000 }, "tag": { "type": "string", "const": "LEZ/message/v1" } } } } ```
## `lez_signIn` [#lez_signin] LWS-0 (needs the lez.wallet.sign\_in intent). **User-facing.** Open it with the `lez.wallet.sign_in` intent; the wallet shows its own sheet. ### Params [#params-12] *No parameters.* ### Result [#result-12] `object`: `account`, `signedMessage`, `signature`
JSON Schema ```json { "params": { "allOf": [ { "type": "object", "required": [ "domain", "uri", "nonce", "issuedAt" ], "properties": { "domain": { "type": "string", "maxLength": 253 }, "statement": { "type": "string", "maxLength": 512 }, "uri": { "type": "string", "maxLength": 2048 }, "nonce": { "type": "string", "minLength": 8, "maxLength": 64 }, "issuedAt": { "type": "string", "format": "date-time" }, "expirationTime": { "type": "string", "format": "date-time" }, "notBefore": { "type": "string", "format": "date-time" }, "requestId": { "type": "string", "maxLength": 64 } } }, { "type": "object", "properties": { "account": { "anyOf": [ { "type": "string", "pattern": "^[1-9A-HJ-NP-Za-km-z]{32,44}$", "description": "Base58-encoded 32-byte LEZ account id" }, { "type": "string", "pattern": "^pvt_[A-Za-z0-9_-]{16,64}$", "description": "Opaque per-origin private account handle" } ] } } } ] }, "result": { "type": "object", "required": [ "account", "signedMessage", "signature" ], "properties": { "account": { "type": "object", "required": [ "address", "kind", "chain", "capabilities" ], "properties": { "address": { "anyOf": [ { "type": "string", "pattern": "^[1-9A-HJ-NP-Za-km-z]{32,44}$", "description": "Base58-encoded 32-byte LEZ account id" }, { "type": "string", "pattern": "^pvt_[A-Za-z0-9_-]{16,64}$", "description": "Opaque per-origin private account handle" } ] }, "kind": { "anyOf": [ { "type": "string", "const": "public" }, { "type": "string", "const": "private" } ] }, "chain": { "type": "string", "pattern": "^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$", "description": "CAIP-2 chain id, e.g. \"lez:testnet\"" }, "publicKey": { "type": "string", "pattern": "^[A-Za-z0-9+/]*={0,2}$", "maxLength": 65000 }, "label": { "type": "string", "maxLength": 64 }, "capabilities": { "type": "array", "items": { "anyOf": [ { "type": "string", "const": "accounts" }, { "type": "string", "const": "read_public" }, { "type": "string", "const": "read_private" }, { "type": "string", "const": "propose_tx" }, { "type": "string", "const": "sign_message" }, { "type": "string", "const": "request_proof" } ] } } } }, "signedMessage": { "type": "string" }, "signature": { "type": "string", "pattern": "^[A-Za-z0-9+/]*={0,2}$", "maxLength": 65000 } } } } ```
## `lez_requestFunds` [#lez_requestfunds] LWS-0 (needs the lez.wallet.request\_funds intent). **User-facing.** Open it with the `lez.wallet.request_funds` intent; the wallet shows its own sheet. ### Behaviour [#behaviour-2] * Opens the wallet's faucet sheet (the testnet faucet, or the debug genesis faucet on a local sequencer). The account must be one the user shared with your app; otherwise `Unauthorized`. * The faucet only pays **public** accounts. For a private target the wallet pays one of the user's public accounts (returned as `fundedAccount`) and queues a shield into the private account for the user to approve (`shieldHandle`). That public account is linked to the private one on chain, and the sheet says so. * `status` is `funded`, `rate_limited` (with `retryAfterSeconds`), `outcome_unknown` or `rejected` (with `reason`). On `outcome_unknown`, don't request again: watch the balance of `fundedAccount` (or the target, when it is public) for a bounded time, because a second request could pay twice. ### Params [#params-13] | Name | Type | Required | Notes | | --------- | ---------------- | -------- | ----------------------------------- | | `chain` | string | yes | CAIP-2 chain id, e.g. "lez:testnet" | | `account` | string \| string | yes | | ### Result [#result-13] `object`: `status`, `amount`, `retryAfterSeconds`, `txHash`, `shieldHandle`, `fundedAccount`, `reason`
JSON Schema ```json { "params": { "type": "object", "required": [ "chain", "account" ], "properties": { "chain": { "type": "string", "pattern": "^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$", "description": "CAIP-2 chain id, e.g. \"lez:testnet\"" }, "account": { "anyOf": [ { "type": "string", "pattern": "^[1-9A-HJ-NP-Za-km-z]{32,44}$", "description": "Base58-encoded 32-byte LEZ account id" }, { "type": "string", "pattern": "^pvt_[A-Za-z0-9_-]{16,64}$", "description": "Opaque per-origin private account handle" } ] } } }, "result": { "type": "object", "required": [ "status" ], "properties": { "status": { "anyOf": [ { "type": "string", "const": "funded" }, { "type": "string", "const": "rate_limited" }, { "type": "string", "const": "rejected" }, { "type": "string", "const": "outcome_unknown" } ] }, "amount": { "type": "string", "pattern": "^(0|[1-9][0-9]{0,38})$", "description": "u128 as a decimal string. The pattern allows up to 39 digits; values above 2^128-1 are rejected by the codec and wallet with -32602." }, "retryAfterSeconds": { "type": "integer", "minimum": 0 }, "txHash": { "type": "string", "pattern": "^0x[0-9a-f]{64}$" }, "shieldHandle": { "type": "string" }, "fundedAccount": { "type": "string", "pattern": "^[1-9A-HJ-NP-Za-km-z]{32,44}$", "description": "Base58-encoded 32-byte LEZ account id" }, "reason": { "type": "string", "maxLength": 256 } } } } ```
## `lez_switchChain` [#lez_switchchain] LWS-0 (needs the wallet UI). **User-facing.** Open it with the `(wallet UI only)` intent; the wallet shows its own sheet. ### Params [#params-14] | Name | Type | Required | Notes | | ------- | ------ | -------- | ----------------------------------- | | `chain` | string | yes | CAIP-2 chain id, e.g. "lez:testnet" | ### Result [#result-14] `null`
JSON Schema ```json { "params": { "type": "object", "required": [ "chain" ], "properties": { "chain": { "type": "string", "pattern": "^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$", "description": "CAIP-2 chain id, e.g. \"lez:testnet\"" } } }, "result": { "type": "null" } } ```
# TypeScript client (/docs/sdk/client) npm pnpm yarn bun ```bash npm i @logos-kit/client @logos-kit/codec ``` ```bash pnpm add @logos-kit/client @logos-kit/codec ``` ```bash yarn add @logos-kit/client @logos-kit/codec ``` ```bash bun add @logos-kit/client @logos-kit/codec ``` `@logos-kit/client` is the typed layer the QML SDK is built on. You can use it directly from Node scripts, tests and tools. It has two sets of actions on one client: * **`nodeActions`**: reads and raw submission against a LEZ node (the sequencer's JSON-RPC). * **`walletActions`**: LWS-0 requests to the Logos Kit wallet (connect, read shared accounts, propose transactions, follow them). Amounts are decimal strings (`u128` never becomes a JS number): lepta for the native token (1 LGO = 10^9 lepta; show them with `formatUnits(amount, 9)` from `@logos-kit/codec`). The root entry point stays QML-safe: no native `BigInt`, no `Intl`. Hover over the code below to see the types. ## Read the chain [#read-the-chain] ```ts twoslash import { createClient, http, nodeActions } from '@logos-kit/client' const node = createClient({ transport: http('https://testnet.lez.logos.co', { retryCount: 2 }), chain: 'lez:testnet', }).extend(nodeActions) async function main() { const height = await node.getBlockNumber() // ^? const fees = await node.getFeeState() // ^? console.log(height, fees.baseFeeExec) } ``` The official LEZ sequencer (`testnet.lez.logos.co`) and a default LEZ deployment send no CORS headers, so from those `http()` works in Node and other non-browser runtimes, not in a web page. In a web page, use Logos Kit's relay for the testnet, `https://lez-testnet.84.46.247.92.sslip.io`, which does send CORS headers. In Basecamp, read through the wallet (`readAccount`, `getWalletBalance`). ## Talk to the wallet [#talk-to-the-wallet] In a Basecamp module, the `basecampModule` transport carries LWS-0 calls to the wallet core module over `logos.callModuleAsync`. The QML SDK sets this up for you; this is what it does underneath: ```ts twoslash import { basecampModule, createClient, walletActions } from '@logos-kit/client' import { nativeTransfer } from '@logos-kit/codec' declare const logos: { callModuleAsync( module: string, method: string, args: unknown[], callback: (payload: string) => void, timeoutMs?: number, ): void } const wallet = createClient({ transport: basecampModule({ callModuleAsync: logos.callModuleAsync }), chain: 'lez:testnet', }).extend(walletActions) async function pay(to: string) { const session = await wallet.connect({ chains: ['lez:testnet'] }) const from = session.accounts[0].address // ^? // Resolves when the user approves, not when the transaction lands. // '1000' is lepta: 0.000001 LGO. const { handle } = await wallet.sendCall(from, nativeTransfer(from, to, '1000')) const status = await wallet.waitForTransactionStatus(handle) // ^? return status.outcome === 'success' } ``` User-facing steps (connect, send, sign, faucet) open a wallet sheet through a Basecamp intent; the rest are reads. See the [QML SDK](/docs/sdk/qml) for the transaction lifecycle and what `outcome` means. ## Types [#types] ### `ClientConfig` [#clientconfig] ### `HttpOptions` [#httpoptions] ### `PollOptions` [#polloptions] Used by every `wait…` action. ### `NodeActions` [#nodeactions] ### `WalletActions` [#walletactions] ### `ProgramCall` (`@logos-kit/codec`) [#programcall-logos-kitcodec] What `sendCall` takes. Build one with the codec's helpers (`nativeTransfer`, the token and testimonial builders) rather than by hand. # Codec (/docs/sdk/codec) `@logos-kit/codec` encodes and decodes what LEZ puts on the wire: account ids, messages, transactions, program instructions and the testimonial program's records. It matches the Rust engine byte for byte (both are checked against the same test vectors), and it runs in Qt's JavaScript engine: no native `BigInt`, `u128` values are decimal strings. npm pnpm yarn bun ```bash npm i @logos-kit/codec ``` ```bash pnpm add @logos-kit/codec ``` ```bash yarn add @logos-kit/codec ``` ```bash bun add @logos-kit/codec ``` ## Program calls [#program-calls] A `ProgramCall` is what `sendCall` proposes: the program, the accounts it touches, and the instruction bytes. ```ts twoslash import { nativeTransfer, tokenTransfer } from '@logos-kit/codec' import type { AccountId } from '@logos-kit/codec' declare const from: AccountId, to: AccountId, token: AccountId const pay = nativeTransfer(from, to, '42') const give = tokenTransfer(from, to, token, '250') // ^? ``` ## Amounts [#amounts] The native token, LOGOS (LGO), has 9 decimals: on the wire and in the SDK its amounts are lepta (1 LGO = 10^9 lepta). `formatUnits` never rounds, and trims trailing zeros. ```ts twoslash import { formatUnits, parseUnits, add, compare } from '@logos-kit/codec' parseUnits('2.5', 9) // "2500000000": LGO → lepta formatUnits('1', 9) // "0.000000001": lepta → LGO parseUnits('1.5', 6) // "1500000" formatUnits('1500000', 6) // "1.5" add('1', '2') // "3" compare('10', '9') // 1 ``` ## Accounts and bytes [#accounts-and-bytes] | Export | | | ------------------------------------------------------------------------------------------------ | -------------------- | | `isAccountId(s)`, `publicAccountId(pubkey)`, `publicPda(program, seed)` | Account ids (base58) | | `NATIVE_TOKEN_PROGRAM`, `TOKEN_PROGRAM`, `ATA_PROGRAM`, `PROGRAM_LOADER`, `builtinProgram(name)` | Built-in programs | | `toBase58`/`fromBase58`, `toBase64`/`fromBase64`, `toHex`/`fromHex`, `utf8` | Bytes | | `sha256(bytes)` | Pure-JS SHA-256 | | `Reader`, `Writer` | Borsh | ## Messages and transactions [#messages-and-transactions] | Export | | | ----------------------------------------------------------------------------------- | ------------------- | | `encodeMessage`, `decodeMessage`, `messageHash` | Public messages | | `encodeTransaction`, `decodeTransaction`, `transactionHash`, `sendTransactionParam` | Signed transactions | | `defaultMaxFee(gasLimit)`, `DEFAULT_GAS_LIMIT` | Fees | ## Testimonials [#testimonials] | Export | | | --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | | `TESTIMONIAL_PROGRAMS` | The deployed program per chain (`lez:testnet` and `lez:preview`) | | `testimonialPost(…)` | The post call (the page account included) | | `decodeTestimonial`, `decodeTestimonialStats` | Records and stats pages | | `TESTIMONIAL_MAX_TEXT` (280), `TESTIMONIAL_MAX_USERNAME` (32), `TESTIMONIAL_PAGE_SIZE` (1000) | The program's limits (bytes) | # QML SDK (/docs/sdk/qml) Copy `sdk/qml/LogosKit/` into your module's `qml/` folder (the [template](/docs/getting-started/quickstart) already has it) and add one object: ```qml import "LogosKit" LogosKit { id: kit visible: root.visible // status polling pauses while hidden onApiChanged: if (api) restore() } ``` `kit.api` is `null` until the SDK is ready, and it's rebuilt when the wallet switches networks: react in `onApiChanged`, and read `kit.api` each time rather than keeping a copy. `kit.sdk` holds helpers (`isUserRejection`, `isAccountId`, `parseUnits`, `formatUnits`, program builders). ## Properties [#properties] | Property | Default | Meaning | | -------------- | -------------------- | ----------------------------------------------------------------------------- | | `chain` | `"lez:testnet"` | The network. Follows the wallet unless `followWallet` is off. | | `followWallet` | `true` | Check the wallet's network on start, when shown, and every 5 s while visible. | | `visible` | `true` | Bind to your view; hidden apps don't poll. | | `module` | `"logos_kit_wallet"` | The wallet core module. | ## Calls [#calls] Every call returns a promise. User-facing ones open the wallet; the rest are reads. | Call | Kind | Resolves to | | ------------------------------------------------------- | ------------ | --------------------------------------------------------------------------------------------------------- | | `connect({ accountKinds, capabilities, signIn })` | wallet sheet | `Session`: `{ sessionId, accounts[], chains, capabilities }` | | `getSession()` | read | `Session` or `null` | | `getAccounts()` | read | the accounts shared with you | | `getChainId()` | read | the wallet's network, e.g. `"lez:testnet"` | | `getWalletBalance(account, asset?)` | read | `{ amount, asset, synced, asOfBlock }` | | `readAccount(account, program)` | read | `{ nonce, data }` (a public account's data for one program) | | `transfer(from, to, amount, token?)` | wallet sheet | `{ handle }` once approved | | `sendCall(account, call)` | wallet sheet | `{ handle }`: any program call built with `kit.sdk` | | `sendTransaction(proposal)` | wallet sheet | `{ handle }`: a raw LWS-0 proposal | | `postTestimonial({ program, author, text, username? })` | wallet sheet | `{ handle }` | | `getTestimonials(program, { limit })` | read | `{ count, monthly, latest[] }` | | `getTestimonial(program, author)` | read | the author's post, or `null` | | `getTransactionStatus(handle)` | read | `TransactionStatus` | | `watchTransaction(handle, onUpdate, onError?)` | read (polls) | a handle with `stop()`; `onUpdate` on each change, stops at the first final lifecycle (`included` counts) | | `waitForTransactionStatus(handle)` | read (polls) | the first final `TransactionStatus` (`included`, `rejected`, `dropped`, `expired`) | | `signMessage(account, base64)` | wallet sheet | `{ signature, publicKey, tag }` | | `signIn(request)` | wallet sheet | `{ account, signedMessage, signature }` | | `requestFunds(account)` | wallet sheet | `{ status, amount?, retryAfterSeconds?, shieldHandle? }` | | `openExplorer({ txHash } \| { account })` | read | opens the explorer in the browser | | `isBusy()` | – | a wallet sheet is open for this app | ## Amounts [#amounts] Amounts are strings in base units: lepta for the native token (1 LGO = 10^9 lepta). That holds for what `transfer` takes and for what `getWalletBalance` and a funded `requestFunds` return. Show them as LGO with `kit.sdk.formatUnits(amount, 9)`, and turn what the user types into lepta with `kit.sdk.parseUnits(text, 9)`. Never `Number()` them. ```qml kit.sdk.formatUnits("2500000000", 9) // "2.5" (LGO) kit.sdk.parseUnits("2.5", 9) // "2500000000" (lepta) ``` ## Transaction lifecycle [#transaction-lifecycle] A proposal resolves when the user **approves**, not when it lands. Follow the handle: ``` awaiting_approval → building → proving → signing → submitted → included ↘ rejected | dropped | expired ``` `watchTransaction` and `waitForTransactionStatus` stop at the first **final** lifecycle: `included`, `rejected`, `dropped` or `expired`. The protocol also reserves `finalized`, but the Logos Kit wallet doesn't report it yet: a transaction's status ends at `included`, with `block.id` set to the block it landed in. If your app needs depth, compare `status.block.id` with the chain's current block later (a node read) instead of waiting on the watcher. Inclusion alone isn't success: LEZ 0.3 includes failed transactions too. Read `outcome` on the final status: | `outcome` | Meaning | Show | | --------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | | `success` | The wallet proved the effect (e.g. your record exists, the balance moved by the amount). | Done | | `failure` | Included, but the program refused it. The fee was charged. | The error, and a retry | | `unknown` | Included, but the wallet couldn't prove the effect either way. | "Included, outcome unconfirmed", then re-read the state you expected to change | Only `success` should show a Done screen. `proving.phase` is set while a private transaction proves (minutes on a laptop). Public calls skip proving. ## One sheet at a time [#one-sheet-at-a-time] While a wallet sheet is open for your app, another user-facing call rejects with `6107` (`RequestPending`). A call that waits 45 s rejects with `6108` (`Timeout`), but the wallet may still be showing it. A timeout does **not** mean nothing was sent: the user can still approve, and the SDK keeps new requests blocked until the wallet really answers. That late answer arrives on the `lateResult` signal. Handle it to recover the handle of a send the user approved after the timeout: ```qml LogosKit { id: kit onLateResult: function (intent, ok, data) { // intent: e.g. "lez.transaction.send"; data: the result, or a LezError when !ok if (intent === "lez.transaction.send" && ok && data.handle) root.follow(data.handle) // same path as an on-time approval else if (!ok && !kit.sdk.isUserRejection(data)) root.showError(data) } } ``` When a call rejects with `6108`, show "Waiting for the wallet" rather than an error, and let `onLateResult` finish the flow. # UI kit (/docs/sdk/ui) `sdk/qml/LogosKitUi/` sits next to `LogosKit/` and gives your app the same look as the wallet. It's optional. ```qml import "LogosKitUi" Card { ColumnLayout { Txt { text: "Balance"; tone: "text2" } Txt { text: kit.sdk.formatUnits(amount, 9) + " LGO"; font.pixelSize: 24; num: true } Btn { text: "Send"; tone: "ink"; large: true; busy: sending; onClicked: send() } } } ``` | Component | Notes | | --------------------------------- | --------------------------------------------------------------------------------------------------- | | `Theme` | Singleton: colours, radii, the bundled Onest font, motion. `Theme.dark` switches palettes. | | `Txt` | Text, **plain by default** (other people's text never renders as rich text). `tone`, `num`, `mono`. | | `Btn` | Pill button: `ink`, `action`, `private`, `neutral`, `ghost`, `danger`; `busy`, `large`, `armDelay`. | | `Card`, `InfoRow` | Surfaces and label/value rows. | | `Notice`, `Tag` | Inline notes (`info`, `warn`, `danger`, `private`) and status tags. | | `Field`, `TextBox` | Inputs. `TextBox.maxBytes` counts UTF-8 bytes, like on-chain limits do. | | `Spinner`, `Skeleton` | Pending and loading states. | | `Identicon`, `LogosMark`, `Glyph` | Account avatars, the official Logos mark, a few line glyphs. | Loading, pending, empty and error states are part of the design: every screen in the testimonial and faucet apps has one, built from these. ## The same tokens outside QML [#the-same-tokens-outside-qml] The Tray colours, radii and type scale also ship as a package, for web pages, tools and anything else that should match the wallet: npm pnpm yarn bun ```bash npm i @logos-kit/theme ``` ```bash pnpm add @logos-kit/theme ``` ```bash yarn add @logos-kit/theme ``` ```bash bun add @logos-kit/theme ``` ```ts import { cssVars, themes } from '@logos-kit/theme' const vars = cssVars(themes.dark) // CSS custom properties ``` # CLI (/docs/wallet/cli) `logos-kit` is the wallet on the command line. It shares the engine, the vault format and the approval policy with the Basecamp wallet. ```sh git clone https://github.com/logos-kit/logos-kit && cd logos-kit cargo xtask lez-vendor # pinned LEZ + our patches into vendor/lez cargo install --path crates/logos-kit-cli logos-kit --help ``` Every command asks before it spends, and shows the decoded request, the fee cap and the request hash. `--yes` skips the prompt only when `LOGOS_KIT_PASSWORD` is set (scripts and CI); `--json` prints machine output. ## Set up [#set-up] ```sh logos-kit init # new wallet; shows the phrase once logos-kit restore --from-date 2026-09-01 # or --from-genesis logos-kit status # zone, sync, network ``` ## Accounts [#accounts] ```sh logos-kit account new # public logos-kit account new --private # private logos-kit account list logos-kit account label "Savings" logos-kit account keys > keys.txt # for others to pay it privately logos-kit balance [--token ] # native balance in LGO ``` ## Funds and transfers [#funds-and-transfers] ```sh logos-kit faucet # testnet: Logos Kit's drip faucet, 1 LGO logos-kit send --from --to --amount 2.5LGO logos-kit send --from --to --amount 42 # a whole number is lepta logos-kit send --from --to --amount 250 --token logos-kit shield --from --to --amount 5000 logos-kit deshield --from --to --amount 100 logos-kit send --from --to-keys keys.txt --amount 10 # someone else's private account ``` `--amount` takes lepta as a whole number (1 LGO = 1,000,000,000 lepta) or LGO written `2.5`, `2.5LGO` or `"3 LGO"`. With `--token` it's a whole number of the token's base units. Human output shows LGO; `--json` stays in lepta. Private routes prove locally (about 4–7 minutes on an M-series Mac). ## Tokens and programs [#tokens-and-programs] ```sh logos-kit token create --name KIT --supply 1000000 --holder logos-kit token list logos-kit program
# header and source verification logos-kit verify-program
# rebuild in docker and compare logos-kit call --from --program

--account --data ``` ## Testimonials [#testimonials] ```sh logos-kit testimonial post --from --text "…Logos Kit…" [--username alice] logos-kit testimonial list logos-kit testimonial evidence --snapshot ./snapshots # adoption evidence from chain data ``` ## Safety [#safety] ```sh logos-kit backup export wallet.backup # encrypted; needs the same password logos-kit backup import wallet.backup logos-kit reveal # the phrase (asks the password again) logos-kit password # change it logos-kit auto-lock 900 # seconds (60–86400) ``` ## Networks [#networks] ```sh logos-kit --zone lez-testnet … # default: the official LEZ testnet (v0.3.0) logos-kit --zone lez-preview … # Logos Kit's earlier 0.3 network (legacy) logos-kit --zone lez-local … # a local sequencer on :3040 logos-kit --zone my-zone --sequencer https://… …# any other zone ``` Or set `LOGOS_KIT_ZONE`. The wallet's files live in `--home` / `LOGOS_KIT_HOME` (default `~/.logos-kit`). # Install the wallet (/docs/wallet/install) ## From the catalog [#from-the-catalog] ### Add the Logos Kit catalog [#add-the-logos-kit-catalog] **Settings → Package Repositories → Add**: ```text https://raw.githubusercontent.com/logos-kit/logos-kit-modules/refs/heads/main/logos-repo.json ``` ### Install [#install] **Applications → Logos Kit Wallet → Install**. Basecamp pulls the core module (`logos_kit_wallet`) with it. ### Set it up [#set-it-up] **Create wallet** (write the recovery phrase down; it's shown once) or **Restore from recovery phrase**. Pick a password; the wallet locks itself after 15 minutes without use (1 minute to 24 hours, in Settings). It starts on the official [LEZ testnet](/docs/concepts/networks). | Package | Version | What | | ----------------------- | ------- | ---------------------------------------- | | `logos_kit_wallet` | 0.2.0 | Core module: keys, policy, proving, sync | | `logos_kit_wallet_ui` | 0.2.0 | The wallet app | | `logos_kit_testimonial` | 0.2.0 | Testimonials app | | `logos_kit_faucet` | 0.2.0 | Test funds app | Packages are signed by the Logos Kit release key and built for macOS (Apple silicon) and Linux (x86-64, arm64). The catalog lives at [logos-kit/logos-kit-modules](https://github.com/logos-kit/logos-kit-modules). ## From source [#from-source] ```sh git clone https://github.com/logos-kit/logos-kit && cd logos-kit cd modules/logos_kit_wallet && nix build .#lgx-portable cd ../logos_kit_wallet_ui && nix build .#lgx-portable ``` Install the two `.lgx` files from `result/` with Basecamp's Package Manager (**Install from file**) or `lgpm install --file .lgx`. Build from a git checkout: the UI flake refers to `../logos_kit_wallet`. ## Using it [#using-it]

* **Accounts**: add public or private accounts from the account menu; name them; switch with one click. * **Send / Receive**: the route (public, shield, deshield, private) follows from the two accounts. Amounts are in LGO, with up to 9 decimals. * **Test LGO**: on the testnet (and the preview network), gets test LGO from Logos Kit's faucet. * **Settings**: network, theme, auto-lock, connected apps (revoke any time), reveal the recovery phrase. # Security model (/docs/wallet/security) Logos Kit targets testnets and hasn't been audited. Report problems privately (see `SECURITY.md` in the repository). ## Where the keys are [#where-the-keys-are] Keys live only in the wallet engine, inside the `logos_kit_wallet` core module. They're encrypted at rest: your password goes through Argon2id once to open a vault holding the recovery phrase and per-network keys, and each network's data is encrypted separately. Apps, the wallet's own UI and the Basecamp shell never receive a key. ## What an app can do [#what-an-app-can-do] An app can **ask**. It can't: * see an account the user didn't share (grants are per app, per account, per capability, and the user can revoke them in Settings → Connected apps); * learn which account a private handle stands for (each app gets its own `pvt_…` handles for the private accounts it was shown); * send anything without the user approving it in the wallet; * open a sheet while another of its requests is open; * make the wallet open an arbitrary link. `openExplorer` builds the URL itself from a fixed testnet explorer base, takes exactly one validated target (a 32-byte transaction hash or an account id), is rate-limited, and answers only apps with a connected session. The app's identity is the name the Basecamp shell attests (`requesterName`), not something the app claims. Beside it, the sheet says whether Basecamp installed the app from a signed package. Basecamp's package manager keeps the publisher's `manifest.sig` next to the install only then. A package without one is marked unsigned, because nothing confirms its publisher. Signed or not, an app can't move funds without the user's approval. ## What an approval shows [#what-an-approval-shows] Before anything is signed, the wallet decodes the proposal and shows: * the balance change first, then each effect it understands; * the program, with its **source verification** (rebuilt from source and matched by image id) and whether its owner can still **upgrade** it; * the fee, and for private transfers, the proof time; * a warning and an explicit acknowledgement for effects it can't decode. Approve buttons arm after 500 ms, so a click meant for something else can't land on them. An approval is bound to the exact request; if chain state moves underneath it (a signer's nonce, an upgradeable program's code), it's refused as stale (`6106`) rather than signed differently. There is one narrow exception, for testimonial posts. A post names the stats page it appends to, and a page holds 1,000 authors. If the page fills while the user is approving, the program would refuse the post and still charge the fee. So right before signing, the wallet re-reads the pages and, only for the trusted testimonial program, rebuilds the **same** post for the open page. The author, text, username, claimed time and signer nonce stay the approved ones, and the rebuilt fee cap may not be higher than the approved one; otherwise the request is refused as stale. ## Privacy [#privacy] Private transactions are proved on the user's machine. The wallet emits no events carrying private data, balances of private accounts are only shared with an app after a separate consent, and a private account is funded from the faucet through one of the user's own public accounts (which is then linked to it on chain; the wallet's faucet sheet says so). ## Programs apps call [#programs-apps-call] The canonical testimonial deployment on the official LEZ testnet is `5YoH3xjhgeKt2mcJXW7c31bqDNCWWA4CRJxVdvzFvVef`: immutable, image `61b2243645ead75c1aec2987bb3aac816e3a4def4c33d38640b55a0457e111a9`, built from commit `4baed37` in the `r0.1.91.1` docker builder, source verification `verified_local`. The SDK lists it as `TESTIMONIAL_PROGRAMS["lez:testnet"]`. Wallets from 0.2.0 recognise only this `v0.3.0` image, so the preview network's testimonial program shows there as an unrecognised program. The wallet treats testimonial posts in two ways, depending on who builds them: * **The wallet's own testimonial intent and the `logos-kit testimonial post` CLI** post only to a trusted deployment: the reproducibly built image, either immutable or named in the wallet's registry. An upgradeable copy of the same image is refused, because its owner could swap the code between approval and inclusion. * **An app's generic program call** (`sendCall`, which the QML SDK's `postTestimonial` uses) can target any program. The wallet doesn't block it, but the approval sheet decodes the post only for a trusted deployment and shows the program's **source verification** and **upgrade** status, so the user sees when a program isn't the verified one. An undecodable call needs an explicit acknowledgement. Posts must be top-level calls: a signer's authorization reaches every program in a chained call, so another program could otherwise post in the user's name.