Developers

Build with Loroco

Loroco runs on a WASM-compiled fork of Sage. This page covers the repo layout, how to get a dev build running, and how to talk to the provider from a dApp.

For dApp authors

Connect from a page

Loroco injects window.loroco (and an opt-in window.chia mirror). The provider shape mirrors Goby / CHIP-0002, so existing dApp code usually works unchanged.

// 1. Detect the provider
const chia = window.loroco ?? window.chia;
if (!chia) throw new Error("Install Loroco");

// 2. Connect — the ONLY entry point. Pops an approval popup.
await chia.request({ method: "connect" });

// 3. Reads work under ANY connection (full OR read-only), no prompt
const [addr] = await chia.request({ method: "accounts" });
const bal   = await chia.request({
  method: "getAssetBalance",
  params: { type: "xch", assetId: null },
});

// 4. Mutating calls re-prompt the user EVERY time — unless the user
//    granted read-only, in which case they're rejected with 4001 and
//    no popup. Always handle 4001 (see "Security levels" below).
const { id } = await chia.request({
  method: "transfer",
  params: { to: "xch1…", amount: "1000000000000", fee: "0" },
});
Namespaces
The same call works as chia_connect, chip0002_connect or connect — the router canonicalises before gating. See the RPC reference for the full surface and error codes.
For dApp authors

Security levels — what your dApp must handle

These are not an API you call. The user controls them at connect time; your job is to degrade gracefully. The wire surface is unchanged from Goby/CHIP-0002 — the only thing you must add is honest handling of 4001.

1 · Connection scope — full vs read-only

A connection is granted at one of two levels:

Your dApp declares the ceiling; the user can only lower it (principle of least privilege). Pass a scope in your connect params to request the least you need:

// Default: requests FULL (write). User may downgrade to read-only.
await chia.request({ method: "connect" });

// A read-only dApp asks for exactly that — the wallet LOCKS it there,
// the user cannot upgrade it to write.
await chia.request({ method: "connect", params: { scope: "read-only" } });
You can request a scope but can't read back what was granted (there's no getScope). You discover read-only the same way you discover "not connected": a signing call returns 4001. If your dApp needs to sign, request "full", and if signing still returns 4001 after a reconnect, tell the user to reconnect choosing full access.

2 · Connections expire (7-day sliding window)

A grant stays alive as long as it's used; after 7 days of inactivity it auto-disconnects. The next call then returns 4001 — exactly like a fresh origin. Treat 4001 on any method as "reconnect needed."

One pattern covers both

Don't assume "connected ⇒ can sign" and don't cache connection state forever. Wrap requests so a 4001 triggers a single reconnect + retry:

const chia = window.loroco ?? window.chia;

async function request(method, params) {
  try {
    return await chia.request({ method, params });
  } catch (e) {
    // 4001 = not connected / expired / blocked by read-only scope
    if (e?.code !== 4001) throw e;

    // Re-establish the connection (pops the approval + scope picker).
    await chia.request({ method: "connect" });

    try {
      return await chia.request({ method, params });
    } catch (e2) {
      // Still 4001 after reconnect ⇒ the user granted READ-ONLY.
      if (e2?.code === 4001 && isSigning(method)) {
        throw new Error(
          "This site needs full access. Reconnect and choose \"Full access\"."
        );
      }
      throw e2;
    }
  }
}

const SIGNING = new Set([
  "transfer", "sendTransaction", "signCoinSpends", "signMessage",
  "signMessageByAddress", "createOffer", "takeOffer", "cancelOffer",
]);
const isSigning = (m) => SIGNING.has(m);

3 · Signing is shown decoded — keep bundles clean

For signCoinSpends / sendTransaction the popup runs the actual CLVM and shows recipients, amounts, fees, value leaving via unrecognised puzzles, and any replayable AGG_SIG_UNSAFE. If the bundle contains anything Loroco can't classify, the user must tick an explicit acknowledgement before Approve unlocks. Practical implications:

4 · Offer royalties are verified on-chain

On takeOffer, Loroco re-derives the royalty amount and destination puzzle hash from the offered NFT's on-chain royalty puzzle — not from anything your offer asserts — and shows it as "verified on-chain." Build honest offers; a mismatched royalty claim is simply ignored in favour of the real one.

TL;DR for integrators
Default behaviour is unchanged from Goby. To be a good citizen of the new model: handle 4001 everywhere (reconnect + retry), request the least scope you need (connect({ scope: "read-only" }) if you never sign), don't assume connect implies signing, keep signable bundles standard and small, and present offers honestly.
Repo shape — read this first

Sage is embedded, not a submodule (today)

The Sage source lives directly under vendor/sage/. This is a deliberate temporary choice — it makes the dev loop simpler (one git clone = everything) while the fork is iterated heavily.

Layout

loroco/
├── packages/
│   ├── extension/         # WXT app (popup + service worker + content script)
│   ├── goby-provider/     # window.loroco + window.chia mirror; CHIP-0002 / WC2 types
│   ├── storage-idb/       # IndexedDB-backed Storage impl for the WASM module
│   └── wallet-wasm/       # wasm-pack output of crates/sage-wasm (regenerated)
├── vendor/
│   ├── sage/              # EMBEDDED Sage Rust source
│   └── chia-wallet-sdk/   # SUBMODULE
├── scripts/               # Playwright smoke + sync benches + security suite
└── .cargo/config.toml     # wasm32-unknown-unknown rustflags
Setup

First-time build

git clone --recurse-submodules git@github.com:MarvinQuevedo/loroco.git
cd loroco
pnpm install

# already cloned? pull the chia-wallet-sdk submodule
git submodule update --init --recursive

You also need:

Daily workflow

Commands

Run all of these from the repo root.

CommandWhat it does
pnpm startFast build (skips wasm rebuild) + launch Chrome with the extension. Use this most of the time.
pnpm start:fullFull build (rebuilds wasm too, ~1 min) + launch Chrome. Run after editing vendor/sage/.
pnpm devWXT dev mode — live reload + auto-launch. Best for iterating on UI.
pnpm chromeOpen Chrome with the existing build (no rebuild). Uses a per-repo profile at ./.chrome-profile/.
pnpm build:fastBuild the extension only — does not touch wasm. ~2 s.
pnpm buildFull build: rebuild wasm + rebuild extension. ~1 min cold.
pnpm zipProduce .output/chrome-mv3.zip for distribution.
pnpm typecheckRun tsc --noEmit across all workspace packages.
Loading in real Chrome
chrome://extensions/ → enable Developer mode → Load unpacked → pick packages/extension/.output/chrome-mv3/. Stable Chrome 148+ ignores --load-extension; the dev scripts use Chrome for Testing with --disable-features=DisableLoadExtensionCommandLineSwitch.
Extending the wallet

Adding a new RPC method

  1. Add to ChiaMethodMap in packages/goby-provider/src/types.ts with exact param/result types.
  2. Register the canonical name in NO_APPROVAL_METHODS (read) or ALWAYS_APPROVAL_METHODS (mutating) in permissions.ts.
  3. Add every chia_* / chip0002_* / snake_case alias to METHOD_ALIASES in rpc-router.ts.
  4. Implement the handler — normalise aliased fields immediately at entry (don't sprinkle x ?? y through the body).
  5. If mutating: add an ApprovalSummary case in popup/App.tsx showing every field the handler will act on.
  6. Add a happy-path assertion to pw-wc-coverage.mjs and an attack case to the security suite if it writes state.
Popup parity
The popup is the last chance to show the user what they're consenting to. If a handler normalises (to ?? address, secure ?? true), the popup must display the normalised value — and never truncate an address/asset id enough to hide a swap.
Sync

coinset.org, or your own sidecar

Loroco syncs against coinset.org over HTTP by default. For real P2P peer sync, run the loroco-local-sync daemon on 127.0.0.1 — the extension auto-detects it via sidecar-client.ts and talks to it over mTLS.

Related repos

loroco-local-sync
The P2P sidecar daemon (split for an independent release cycle).
loroco-wallet
Older variant where Sage is a git submodule — the target future state.
MarvinQuevedo/sage
Upstream of the embedded Sage source (branch web/coinset-sync).