From e0ee65bb46e920ce78af41c9fa2e74117a993f7e Mon Sep 17 00:00:00 2001 From: johnnymatthews <9611008+johnnymatthews@users.noreply.github.com> Date: Thu, 3 Sep 2026 08:55:04 +0000 Subject: [PATCH 01/11] Adds complete tree structure for 'Build apps' section. --- config.toml | 33 +++++++++++++++---- content/build/_index.md | 4 --- content/build/create-a-view/index.md | 2 +- .../attestation-as-a-query-filter/index.md | 6 ++++ .../choosing-an-architecture/index.md | 6 ++++ .../build/explanation/the-app-model/index.md | 6 ++++ .../explanation/views-for-builders/index.md | 6 ++++ .../configure-attestation-thresholds/index.md | 6 ++++ .../build/how-to/connect-to-a-host/index.md | 6 ++++ .../how-to/find-views-and-hosts/index.md | 6 ++++ content/build/how-to/query-data/index.md | 6 ++++ .../build/how-to/subscribe-to-views/index.md | 6 ++++ .../build/how-to/use-shinzo-studio/index.md | 6 ++++ .../use-your-own-infrastructure/index.md | 6 ++++ content/build/how-to/verify-data/index.md | 6 ++++ content/build/how-to/view-recipes/index.md | 6 ++++ content/build/how-to/write-a-lens/index.md | 6 ++++ content/build/index.md | 6 ++++ .../build-a-local-first-app/index.md | 6 ++++ .../tutorials/create-your-first-view/index.md | 6 ++++ .../tutorials/query-your-first-view/index.md | 6 ++++ 21 files changed, 135 insertions(+), 12 deletions(-) delete mode 100644 content/build/_index.md create mode 100644 content/build/explanation/attestation-as-a-query-filter/index.md create mode 100644 content/build/explanation/choosing-an-architecture/index.md create mode 100644 content/build/explanation/the-app-model/index.md create mode 100644 content/build/explanation/views-for-builders/index.md create mode 100644 content/build/how-to/configure-attestation-thresholds/index.md create mode 100644 content/build/how-to/connect-to-a-host/index.md create mode 100644 content/build/how-to/find-views-and-hosts/index.md create mode 100644 content/build/how-to/query-data/index.md create mode 100644 content/build/how-to/subscribe-to-views/index.md create mode 100644 content/build/how-to/use-shinzo-studio/index.md create mode 100644 content/build/how-to/use-your-own-infrastructure/index.md create mode 100644 content/build/how-to/verify-data/index.md create mode 100644 content/build/how-to/view-recipes/index.md create mode 100644 content/build/how-to/write-a-lens/index.md create mode 100644 content/build/index.md create mode 100644 content/build/tutorials/build-a-local-first-app/index.md create mode 100644 content/build/tutorials/create-your-first-view/index.md create mode 100644 content/build/tutorials/query-your-first-view/index.md diff --git a/config.toml b/config.toml index 00bd154..ffa5240 100644 --- a/config.toml +++ b/config.toml @@ -70,7 +70,7 @@ section = "run" [[extra.navbar]] label = "Build apps" -href = "/build/create-a-view/" +href = "/build/" section = "build" [[extra.navbar]] @@ -126,18 +126,37 @@ pages = [ # Build apps [[extra.sidebar]] section = "build" -label = "Develop" +label = "Tutorials" pages = [ - "build/create-a-view/index.md", - "build/build-an-app/index.md", - "build/query-data/index.md", + "build/tutorials/query-your-first-view/index.md", + "build/tutorials/create-your-first-view/index.md", + "build/tutorials/build-a-local-first-app/index.md", ] [[extra.sidebar]] section = "build" -label = "Concepts" +label = "How-to guides" pages = [ - "build/concepts/views-for-builders/index.md", + "build/how-to/find-views-and-hosts/index.md", + "build/how-to/connect-to-a-host/index.md", + "build/how-to/query-data/index.md", + "build/how-to/view-recipes/index.md", + "build/how-to/use-shinzo-studio/index.md", + "build/how-to/write-a-lens/index.md", + "build/how-to/subscribe-to-views/index.md", + "build/how-to/configure-attestation-thresholds/index.md", + "build/how-to/verify-data/index.md", + "build/how-to/use-your-own-infrastructure/index.md", +] + +[[extra.sidebar]] +section = "build" +label = "Explanation" +pages = [ + "build/explanation/the-app-model/index.md", + "build/explanation/choosing-an-architecture/index.md", + "build/explanation/views-for-builders/index.md", + "build/explanation/attestation-as-a-query-filter/index.md", ] # Run infrastructure diff --git a/content/build/_index.md b/content/build/_index.md deleted file mode 100644 index 4a4aaf5..0000000 --- a/content/build/_index.md +++ /dev/null @@ -1,4 +0,0 @@ -+++ -render = false -page_template = "page.html" -+++ diff --git a/content/build/create-a-view/index.md b/content/build/create-a-view/index.md index 8be5073..b53fa07 100644 --- a/content/build/create-a-view/index.md +++ b/content/build/create-a-view/index.md @@ -1,6 +1,6 @@ +++ title = "Create a View" -aliases = ["/views/quickstart", "/views/install", "/build"] +aliases = ["/views/quickstart", "/views/install"] description = "Build and deploy your first Shinzo View with Viewkit — from install to local testing to devnet publishing." +++ Viewkit is a CLI tool that helps you initialize, manage, and publish Shinzo views. In this guide we're going to build and install the `viewkit` executable, create a view, and publish it to the testnet. If you'd rather work from a browser UI instead of the CLI, [Shinzo Studio](https://studio.shinzo.network/) covers the same create → deploy → query flow for Views. diff --git a/content/build/explanation/attestation-as-a-query-filter/index.md b/content/build/explanation/attestation-as-a-query-filter/index.md new file mode 100644 index 0000000..8e4aa38 --- /dev/null +++ b/content/build/explanation/attestation-as-a-query-filter/index.md @@ -0,0 +1,6 @@ ++++ +title = "Attestation as a query filter" +description = "Why attestation thresholds are a per-query trust dial, and how pushed attestation records let each app set its own bar for accepting data." ++++ + +This page is coming soon. diff --git a/content/build/explanation/choosing-an-architecture/index.md b/content/build/explanation/choosing-an-architecture/index.md new file mode 100644 index 0000000..c491bab --- /dev/null +++ b/content/build/explanation/choosing-an-architecture/index.md @@ -0,0 +1,6 @@ ++++ +title = "Choosing an app architecture" +description = "Trade-offs between the three ways to build on Shinzo: direct signed queries to Hosts, an embedded local-first client, or your own private Host." ++++ + +This page is coming soon. diff --git a/content/build/explanation/the-app-model/index.md b/content/build/explanation/the-app-model/index.md new file mode 100644 index 0000000..11eecc9 --- /dev/null +++ b/content/build/explanation/the-app-model/index.md @@ -0,0 +1,6 @@ ++++ +title = "The Shinzo app model" +description = "How Shinzo inverts the traditional data-API model: you define the API, and pre-processed data is pushed to your application as a verifiable local cache." ++++ + +This page is coming soon. diff --git a/content/build/explanation/views-for-builders/index.md b/content/build/explanation/views-for-builders/index.md new file mode 100644 index 0000000..a2d0b67 --- /dev/null +++ b/content/build/explanation/views-for-builders/index.md @@ -0,0 +1,6 @@ ++++ +title = "Views for builders" +description = "What a Shinzo View is: a versioned bundle of query, SDL, and lenses, and how Viewkit turns primitive data into reusable data APIs." ++++ + +This page is coming soon. diff --git a/content/build/how-to/configure-attestation-thresholds/index.md b/content/build/how-to/configure-attestation-thresholds/index.md new file mode 100644 index 0000000..5f722c8 --- /dev/null +++ b/content/build/how-to/configure-attestation-thresholds/index.md @@ -0,0 +1,6 @@ ++++ +title = "Configure attestation thresholds" +description = "How to require a minimum number of Generator attestations before query results are returned in your app." ++++ + +This page is coming soon. diff --git a/content/build/how-to/connect-to-a-host/index.md b/content/build/how-to/connect-to-a-host/index.md new file mode 100644 index 0000000..18b9035 --- /dev/null +++ b/content/build/how-to/connect-to-a-host/index.md @@ -0,0 +1,6 @@ ++++ +title = "Connect your app to a Host" +description = "How to connect an application to a Shinzo Host: P2P connection strings for embedded Go apps and GraphQL endpoints for direct-query TypeScript apps." ++++ + +This page is coming soon. diff --git a/content/build/how-to/find-views-and-hosts/index.md b/content/build/how-to/find-views-and-hosts/index.md new file mode 100644 index 0000000..627bf35 --- /dev/null +++ b/content/build/how-to/find-views-and-hosts/index.md @@ -0,0 +1,6 @@ ++++ +title = "Find Views and Hosts" +description = "How to discover registered Views, serving Hosts, and pools on the Shinzo network using the Explorer, Studio, the REST API, or the TypeScript SDK." ++++ + +This page is coming soon. diff --git a/content/build/how-to/query-data/index.md b/content/build/how-to/query-data/index.md new file mode 100644 index 0000000..9461817 --- /dev/null +++ b/content/build/how-to/query-data/index.md @@ -0,0 +1,6 @@ ++++ +title = "Query data" +description = "How to query Shinzo data with GraphQL: filters, ordering, nested documents, DocIDs, CIDs, and signatures." ++++ + +This page is coming soon. diff --git a/content/build/how-to/subscribe-to-views/index.md b/content/build/how-to/subscribe-to-views/index.md new file mode 100644 index 0000000..9898a5e --- /dev/null +++ b/content/build/how-to/subscribe-to-views/index.md @@ -0,0 +1,6 @@ ++++ +title = "Subscribe to Views with the app-sdk" +description = "How to configure and start an embedded DefraDB instance, subscribe to Views, and receive pushed data in a Go application." ++++ + +This page is coming soon. diff --git a/content/build/how-to/use-shinzo-studio/index.md b/content/build/how-to/use-shinzo-studio/index.md new file mode 100644 index 0000000..02a69c0 --- /dev/null +++ b/content/build/how-to/use-shinzo-studio/index.md @@ -0,0 +1,6 @@ ++++ +title = "Create and deploy Views in Shinzo Studio" +description = "How to create, deploy, and query Views from your browser with Shinzo Studio, without installing the CLI." ++++ + +This page is coming soon. diff --git a/content/build/how-to/use-your-own-infrastructure/index.md b/content/build/how-to/use-your-own-infrastructure/index.md new file mode 100644 index 0000000..b54b88f --- /dev/null +++ b/content/build/how-to/use-your-own-infrastructure/index.md @@ -0,0 +1,6 @@ ++++ +title = "Use your own infrastructure" +description = "How to run your own Host as a private Direct Client for your app: the strongest privacy and control, at the cost of operating a node." ++++ + +This page is coming soon. diff --git a/content/build/how-to/verify-data/index.md b/content/build/how-to/verify-data/index.md new file mode 100644 index 0000000..6023562 --- /dev/null +++ b/content/build/how-to/verify-data/index.md @@ -0,0 +1,6 @@ ++++ +title = "Verify data with signatures and CIDs" +description = "How to verify who signed your data and navigate attestations, commits, and documents via CIDs." ++++ + +This page is coming soon. diff --git a/content/build/how-to/view-recipes/index.md b/content/build/how-to/view-recipes/index.md new file mode 100644 index 0000000..176b7db --- /dev/null +++ b/content/build/how-to/view-recipes/index.md @@ -0,0 +1,6 @@ ++++ +title = "View recipes" +description = "How to build common Views: decode event logs, filter by contract, decode multiple event types, transaction Views, and editing or rolling back a View." ++++ + +This page is coming soon. diff --git a/content/build/how-to/write-a-lens/index.md b/content/build/how-to/write-a-lens/index.md new file mode 100644 index 0000000..77bb403 --- /dev/null +++ b/content/build/how-to/write-a-lens/index.md @@ -0,0 +1,6 @@ ++++ +title = "Write and test a custom lens" +description = "How to author a WebAssembly lens with the AssemblyScript SDK, test it locally, and attach it to a View." ++++ + +This page is coming soon. diff --git a/content/build/index.md b/content/build/index.md new file mode 100644 index 0000000..3851fc7 --- /dev/null +++ b/content/build/index.md @@ -0,0 +1,6 @@ ++++ +title = "Build apps" +description = "Choose your path for building on Shinzo: query existing Views, create your own View, or build a local-first app." ++++ + +This page is coming soon. diff --git a/content/build/tutorials/build-a-local-first-app/index.md b/content/build/tutorials/build-a-local-first-app/index.md new file mode 100644 index 0000000..95c5878 --- /dev/null +++ b/content/build/tutorials/build-a-local-first-app/index.md @@ -0,0 +1,6 @@ ++++ +title = "Build a local-first app" +description = "Tutorial: build a Go app that embeds DefraDB via the app-sdk, subscribes to a View, and queries pushed data locally with attestation filtering." ++++ + +This page is coming soon. diff --git a/content/build/tutorials/create-your-first-view/index.md b/content/build/tutorials/create-your-first-view/index.md new file mode 100644 index 0000000..be223d0 --- /dev/null +++ b/content/build/tutorials/create-your-first-view/index.md @@ -0,0 +1,6 @@ ++++ +title = "Create your first View" +description = "Tutorial: build, test, and deploy a Shinzo View with Viewkit, from install to querying it on the public testnet." ++++ + +This page is coming soon. diff --git a/content/build/tutorials/query-your-first-view/index.md b/content/build/tutorials/query-your-first-view/index.md new file mode 100644 index 0000000..582f7f9 --- /dev/null +++ b/content/build/tutorials/query-your-first-view/index.md @@ -0,0 +1,6 @@ ++++ +title = "Query your first View" +description = "Tutorial: query a live View on the Shinzo testnet from a TypeScript app. Discover a View, pick a Host, sign a request, and print results." ++++ + +This page is coming soon. From 35885ffe8b43fcddb198b85dc7445724b420ee01 Mon Sep 17 00:00:00 2001 From: johnnymatthews <9611008+johnnymatthews@users.noreply.github.com> Date: Thu, 3 Sep 2026 20:32:17 +0000 Subject: [PATCH 02/11] First draft of local-apps build. --- .../build-a-local-first-app/index.md | 219 +++++++++++++++++- 1 file changed, 217 insertions(+), 2 deletions(-) diff --git a/content/build/tutorials/build-a-local-first-app/index.md b/content/build/tutorials/build-a-local-first-app/index.md index 95c5878..06e8750 100644 --- a/content/build/tutorials/build-a-local-first-app/index.md +++ b/content/build/tutorials/build-a-local-first-app/index.md @@ -1,6 +1,221 @@ +++ title = "Build a local-first app" -description = "Tutorial: build a Go app that embeds DefraDB via the app-sdk, subscribes to a View, and queries pushed data locally with attestation filtering." +description = "Tutorial: build a Go app that embeds DefraDB via the app-sdk, subscribes to a View, and queries pushed data locally." +++ -This page is coming soon. +A local-first app gets its blockchain data pushed to it. You embed a DefraDB instance inside your Go app with the app-sdk, subscribe to a View, and Host clients push the View's pre-processed data to your app over P2P. After that, queries run against your local copy: no per-query API calls, and the data is verifiable because every document carries signatures from the Generator clients that wrote it. For the reasoning behind this model, see [The Shinzo app model](/build/explanation/the-app-model/). + +By the end of this tutorial you have a running Go program that subscribes to a View hosted on the public testnet and answers queries locally. You do not need a wallet, and you do not run a Generator or Host client yourself. The only setup is the Go toolchain. + +{% admonition(type="warning") %} +Pushed replication is currently blocked by a version mismatch. The app-sdk pins DefraDB v0.20 while public Host clients run DefraDB v1.0, and documents published by a v1.0 Host cannot be parsed by a v0.20 app, so nothing arrives yet. Every step below is correct against the current app-sdk: your app connects and subscribes successfully, and its queries return empty until the SDK ships a compatible DefraDB. If you need data in a Go app today, query a Host directly instead, as described in [Query data](/build/how-to/query-data/). +{% end %} + +## Before you start + +- Go 1.25 or later. +- A C compiler such as gcc. DefraDB pulls in a native module, so builds need CGO enabled. On macOS and most Linux machines with a standard Go install this works out of the box. +- About 15 minutes. + +## Scaffold the app + +1. Create a new module and pull the app-sdk: + + ```shell + mkdir shinzo-app + cd shinzo-app + go mod init shinzo-app + go get github.com/shinzonetwork/shinzo-app-sdk + ``` + + {% admonition(type="note") %} +`go get github.com/shinzonetwork/app-sdk` fails with a module path error. The repository was renamed and the module now declares `github.com/shinzonetwork/shinzo-app-sdk`. + {% end %} + +1. Create `main.go` with the imports the rest of the tutorial uses: + + ```go + package main + + import ( + "context" + "strings" + + "github.com/shinzonetwork/shinzo-app-sdk/pkg/config" + "github.com/shinzonetwork/shinzo-app-sdk/pkg/defra" + "github.com/shinzonetwork/shinzo-app-sdk/pkg/views" + ) + ``` + +## Configure the app + +The app-sdk loads a `config.yaml` at startup. Create one with these contents: + +```yaml +defradb: + url: "http://localhost:9181" + keyring_secret: "dev-secret" + p2p: + enabled: true + bootstrap_peers: [] + listen_addr: "/ip4/127.0.0.1/tcp/9171" + store: + path: "./.defra" + +shinzo: + minimum_attestations: 1 + +logger: + development: true +``` + +A few keys matter more than the rest: + +- `defradb.keyring_secret` is required. It encrypts the local keyring that holds your node's identity, so your app keeps the same P2P identity across restarts. You can also set it through the `DEFRA_KEYRING_SECRET` environment variable instead of the file. +- `defradb.p2p.enabled` must be `true`. Without it the SDK starts with networking off and no data can be pushed to you. +- `shinzo.minimum_attestations` sets the default attestation threshold used when filtering queries. The tutorial does not use attestation filtering, so this only needs a syntactically valid value. +- `logger.development` keeps DefraDB's logs visible while you learn. Set it to `false` in production. + +## Connect to a Host + +Your embedded DefraDB instance discovers Host clients by dialing bootstrap peers. Registered Host clients publish their connection strings on-chain, and the testnet registry exposes them over REST. + +1. List the registered Host clients: + + ```shell + curl -s http://testnet.shinzo.network:1317/shinzonetwork/host/v1/hosts | jq -r '.hosts[].connection_string' + ``` + + ```output + /ip4/34.63.186.249/tcp/9171/p2p/12D3KooWSqvLctTtcQLvqSVZU4sTCUWxCX9z4NeFpSHnmVWBiFMZ + /ip4/65.109.106.214/tcp/9171/p2p/12D3KooWCZgmwi1Kz6Sjqkpm4b8b4D5Hvb82KwyFPpGRZPuhFENB + ... + ``` + +1. Pick one or two connection strings and add them to `bootstrap_peers` in your `config.yaml`: + + ```yaml + defradb: + p2p: + enabled: true + bootstrap_peers: + - "/ip4/34.63.186.249/tcp/9171/p2p/12D3KooWSqvLctTtcQLvqSVZU4sTCUWxCX9z4NeFpSHnmVWBiFMZ" + listen_addr: "/ip4/127.0.0.1/tcp/9171" + ``` + +The registry moves over time, so if dialing fails, pull the list again and swap in a current Host. For more ways to find Hosts and what each field in the registry means, see [Find Views and Hosts](/build/how-to/find-views-and-hosts/) and [Connect your app to a Host](/build/how-to/connect-to-a-host/). + +{% admonition(type="tip") %} +Running your own Generator client and Host client locally for development? Point `bootstrap_peers` at your Host instead, for example `/ip4/127.0.0.1/tcp/9171/p2p/`. +{% end %} + +## Start the embedded DefraDB instance + +Add the startup code to `main.go`: + +1. Load the config you wrote: + + ```go + shinzoConfig, err := config.LoadConfig("config.yaml") + if err != nil { + panic(err) + } + ``` + +1. Start DefraDB and close it when the program exits: + + ```go + myNode, _, err := defra.StartDefraInstance( + shinzoConfig, + &defra.MockSchemaApplierThatSucceeds{}, + nil, + nil, + ) + if err != nil { + panic(err) + } + defer myNode.Close(context.Background()) + ``` + + `MockSchemaApplierThatSucceeds` is the schema applier to use when DefraDB only holds Shinzo data. If your app also stores its own documents in DefraDB, use `SchemaApplierFromFile` or `SchemaApplierFromProvidedSchema` instead, and put your schema there. The two `nil` arguments are optional node options and a replication filter, which this tutorial does not need, and the second return value is the network handler, which you can ignore here. + +## Subscribe to the View + +Subscribing does two things: it applies the View's SDL to your embedded DefraDB instance so the collection exists locally, and it registers that collection with Defra's passive replication so Host clients know to push its documents to you. + +The tutorial uses `Studio_v1_Erc20TransferUSDC`, a View already registered on the public testnet. It decodes Transfer events from a token contract and exposes them as documents with token address, sender, recipient, amount, and block number. If you already finished [Create your first View](/build/tutorials/create-your-first-view/), you can substitute that View instead. + +1. Define the View and subscribe to it: + + ```go + sdl := `type Studio_v1_Erc20TransferUSDC { + tokenAddress: String + hash: String + blockNumber: Int + from: String + to: String + amount: String + }` + + view := views.View{ + Name: "Studio_v1_Erc20TransferUSDC", + Sdl: &sdl, + } + + err = view.SubscribeTo(context.Background(), myNode) + if err != nil { + if strings.Contains(err.Error(), "collection already exists") { + // You have subscribed before. The error is informational and safe to ignore. + } else { + panic(err) + } + } + ``` + + {% admonition(type="note") %} +The View's SDL as registered on the hub includes the directive `@materialized(if: false)`, which tells the Host client to compute results on query instead of storing them. The DefraDB version the app-sdk embeds rejects that directive, so the app applies the SDL without it and stores pushed documents locally. Only the `Name`, `Sdl`, and optionally `Query` fields of the View struct matter for subscribing. + {% end %} + +## Query your data + +Host clients push the View's documents into your local collection as they process new blocks. Once subscribed, you query the collection with the SDK's helper and a Go struct that matches the fields. + +1. Describe the result type: + + ```go + type Transfer struct { + TokenAddress string `json:"tokenAddress"` + From string `json:"from"` + To string `json:"to"` + Amount string `json:"amount"` + BlockNumber int `json:"blockNumber"` + } + ``` + +1. Query with `defra.QueryArray`: + + ```go + transfers, err := defra.QueryArray[Transfer]( + context.Background(), + myNode, + `query { Studio_v1_Erc20TransferUSDC(limit: 10) { tokenAddress from to amount blockNumber } }`, + ) + if err != nil { + panic(err) + } + ``` + +Until pushed replication works across the DefraDB version gap described at the top, this returns an empty slice. Once a compatible app-sdk release lands, you will see the slice fill with transfers a few moments after subscribing, and the same program keeps receiving new data as long as the Host clients keep running. For richer querying, `defra.QuerySingle` fetches one document. For all the GraphQL filters and ordering you can use here, see [Query data](/build/how-to/query-data/). + +That is the whole local-first flow: subscribe once, then treat the embedded DefraDB instance as your read model. + +## Where to next + +- [Create your first View](/build/tutorials/create-your-first-view/) to define your own data instead of using a public View. +- [Subscribe to Views with the app-sdk](/build/how-to/subscribe-to-views/) for the full configuration surface, including schema appliers and clean shutdown. +- [The Shinzo app model](/build/explanation/the-app-model/) for the concepts behind pushed data and local querying. +- [Choosing an app architecture](/build/explanation/choosing-an-architecture/) to compare this with direct signed queries and running your own Host. + +## Need help + +{{ need_help(client="app-sdk", repo_name="app-sdk", repo="https://github.com/shinzonetwork/app-sdk/issues") }} From e401792f5970fa64d2d22d85eba8fa3ff0472106 Mon Sep 17 00:00:00 2001 From: johnnymatthews <9611008+johnnymatthews@users.noreply.github.com> Date: Thu, 3 Sep 2026 08:55:04 +0000 Subject: [PATCH 03/11] Adds complete tree structure for 'Build apps' section. --- config.toml | 33 +++++++++++++++---- content/build/_index.md | 4 --- content/build/create-a-view/index.md | 2 +- .../attestation-as-a-query-filter/index.md | 6 ++++ .../choosing-an-architecture/index.md | 6 ++++ .../build/explanation/the-app-model/index.md | 6 ++++ .../explanation/views-for-builders/index.md | 6 ++++ .../configure-attestation-thresholds/index.md | 6 ++++ .../build/how-to/connect-to-a-host/index.md | 6 ++++ .../how-to/find-views-and-hosts/index.md | 6 ++++ content/build/how-to/query-data/index.md | 6 ++++ .../build/how-to/subscribe-to-views/index.md | 6 ++++ .../build/how-to/use-shinzo-studio/index.md | 6 ++++ .../use-your-own-infrastructure/index.md | 6 ++++ content/build/how-to/verify-data/index.md | 6 ++++ content/build/how-to/view-recipes/index.md | 6 ++++ content/build/how-to/write-a-lens/index.md | 6 ++++ content/build/index.md | 6 ++++ .../build-a-local-first-app/index.md | 6 ++++ .../tutorials/create-your-first-view/index.md | 6 ++++ .../tutorials/query-your-first-view/index.md | 6 ++++ 21 files changed, 135 insertions(+), 12 deletions(-) delete mode 100644 content/build/_index.md create mode 100644 content/build/explanation/attestation-as-a-query-filter/index.md create mode 100644 content/build/explanation/choosing-an-architecture/index.md create mode 100644 content/build/explanation/the-app-model/index.md create mode 100644 content/build/explanation/views-for-builders/index.md create mode 100644 content/build/how-to/configure-attestation-thresholds/index.md create mode 100644 content/build/how-to/connect-to-a-host/index.md create mode 100644 content/build/how-to/find-views-and-hosts/index.md create mode 100644 content/build/how-to/query-data/index.md create mode 100644 content/build/how-to/subscribe-to-views/index.md create mode 100644 content/build/how-to/use-shinzo-studio/index.md create mode 100644 content/build/how-to/use-your-own-infrastructure/index.md create mode 100644 content/build/how-to/verify-data/index.md create mode 100644 content/build/how-to/view-recipes/index.md create mode 100644 content/build/how-to/write-a-lens/index.md create mode 100644 content/build/index.md create mode 100644 content/build/tutorials/build-a-local-first-app/index.md create mode 100644 content/build/tutorials/create-your-first-view/index.md create mode 100644 content/build/tutorials/query-your-first-view/index.md diff --git a/config.toml b/config.toml index 00bd154..ffa5240 100644 --- a/config.toml +++ b/config.toml @@ -70,7 +70,7 @@ section = "run" [[extra.navbar]] label = "Build apps" -href = "/build/create-a-view/" +href = "/build/" section = "build" [[extra.navbar]] @@ -126,18 +126,37 @@ pages = [ # Build apps [[extra.sidebar]] section = "build" -label = "Develop" +label = "Tutorials" pages = [ - "build/create-a-view/index.md", - "build/build-an-app/index.md", - "build/query-data/index.md", + "build/tutorials/query-your-first-view/index.md", + "build/tutorials/create-your-first-view/index.md", + "build/tutorials/build-a-local-first-app/index.md", ] [[extra.sidebar]] section = "build" -label = "Concepts" +label = "How-to guides" pages = [ - "build/concepts/views-for-builders/index.md", + "build/how-to/find-views-and-hosts/index.md", + "build/how-to/connect-to-a-host/index.md", + "build/how-to/query-data/index.md", + "build/how-to/view-recipes/index.md", + "build/how-to/use-shinzo-studio/index.md", + "build/how-to/write-a-lens/index.md", + "build/how-to/subscribe-to-views/index.md", + "build/how-to/configure-attestation-thresholds/index.md", + "build/how-to/verify-data/index.md", + "build/how-to/use-your-own-infrastructure/index.md", +] + +[[extra.sidebar]] +section = "build" +label = "Explanation" +pages = [ + "build/explanation/the-app-model/index.md", + "build/explanation/choosing-an-architecture/index.md", + "build/explanation/views-for-builders/index.md", + "build/explanation/attestation-as-a-query-filter/index.md", ] # Run infrastructure diff --git a/content/build/_index.md b/content/build/_index.md deleted file mode 100644 index 4a4aaf5..0000000 --- a/content/build/_index.md +++ /dev/null @@ -1,4 +0,0 @@ -+++ -render = false -page_template = "page.html" -+++ diff --git a/content/build/create-a-view/index.md b/content/build/create-a-view/index.md index 8be5073..b53fa07 100644 --- a/content/build/create-a-view/index.md +++ b/content/build/create-a-view/index.md @@ -1,6 +1,6 @@ +++ title = "Create a View" -aliases = ["/views/quickstart", "/views/install", "/build"] +aliases = ["/views/quickstart", "/views/install"] description = "Build and deploy your first Shinzo View with Viewkit — from install to local testing to devnet publishing." +++ Viewkit is a CLI tool that helps you initialize, manage, and publish Shinzo views. In this guide we're going to build and install the `viewkit` executable, create a view, and publish it to the testnet. If you'd rather work from a browser UI instead of the CLI, [Shinzo Studio](https://studio.shinzo.network/) covers the same create → deploy → query flow for Views. diff --git a/content/build/explanation/attestation-as-a-query-filter/index.md b/content/build/explanation/attestation-as-a-query-filter/index.md new file mode 100644 index 0000000..8e4aa38 --- /dev/null +++ b/content/build/explanation/attestation-as-a-query-filter/index.md @@ -0,0 +1,6 @@ ++++ +title = "Attestation as a query filter" +description = "Why attestation thresholds are a per-query trust dial, and how pushed attestation records let each app set its own bar for accepting data." ++++ + +This page is coming soon. diff --git a/content/build/explanation/choosing-an-architecture/index.md b/content/build/explanation/choosing-an-architecture/index.md new file mode 100644 index 0000000..c491bab --- /dev/null +++ b/content/build/explanation/choosing-an-architecture/index.md @@ -0,0 +1,6 @@ ++++ +title = "Choosing an app architecture" +description = "Trade-offs between the three ways to build on Shinzo: direct signed queries to Hosts, an embedded local-first client, or your own private Host." ++++ + +This page is coming soon. diff --git a/content/build/explanation/the-app-model/index.md b/content/build/explanation/the-app-model/index.md new file mode 100644 index 0000000..11eecc9 --- /dev/null +++ b/content/build/explanation/the-app-model/index.md @@ -0,0 +1,6 @@ ++++ +title = "The Shinzo app model" +description = "How Shinzo inverts the traditional data-API model: you define the API, and pre-processed data is pushed to your application as a verifiable local cache." ++++ + +This page is coming soon. diff --git a/content/build/explanation/views-for-builders/index.md b/content/build/explanation/views-for-builders/index.md new file mode 100644 index 0000000..a2d0b67 --- /dev/null +++ b/content/build/explanation/views-for-builders/index.md @@ -0,0 +1,6 @@ ++++ +title = "Views for builders" +description = "What a Shinzo View is: a versioned bundle of query, SDL, and lenses, and how Viewkit turns primitive data into reusable data APIs." ++++ + +This page is coming soon. diff --git a/content/build/how-to/configure-attestation-thresholds/index.md b/content/build/how-to/configure-attestation-thresholds/index.md new file mode 100644 index 0000000..5f722c8 --- /dev/null +++ b/content/build/how-to/configure-attestation-thresholds/index.md @@ -0,0 +1,6 @@ ++++ +title = "Configure attestation thresholds" +description = "How to require a minimum number of Generator attestations before query results are returned in your app." ++++ + +This page is coming soon. diff --git a/content/build/how-to/connect-to-a-host/index.md b/content/build/how-to/connect-to-a-host/index.md new file mode 100644 index 0000000..18b9035 --- /dev/null +++ b/content/build/how-to/connect-to-a-host/index.md @@ -0,0 +1,6 @@ ++++ +title = "Connect your app to a Host" +description = "How to connect an application to a Shinzo Host: P2P connection strings for embedded Go apps and GraphQL endpoints for direct-query TypeScript apps." ++++ + +This page is coming soon. diff --git a/content/build/how-to/find-views-and-hosts/index.md b/content/build/how-to/find-views-and-hosts/index.md new file mode 100644 index 0000000..627bf35 --- /dev/null +++ b/content/build/how-to/find-views-and-hosts/index.md @@ -0,0 +1,6 @@ ++++ +title = "Find Views and Hosts" +description = "How to discover registered Views, serving Hosts, and pools on the Shinzo network using the Explorer, Studio, the REST API, or the TypeScript SDK." ++++ + +This page is coming soon. diff --git a/content/build/how-to/query-data/index.md b/content/build/how-to/query-data/index.md new file mode 100644 index 0000000..9461817 --- /dev/null +++ b/content/build/how-to/query-data/index.md @@ -0,0 +1,6 @@ ++++ +title = "Query data" +description = "How to query Shinzo data with GraphQL: filters, ordering, nested documents, DocIDs, CIDs, and signatures." ++++ + +This page is coming soon. diff --git a/content/build/how-to/subscribe-to-views/index.md b/content/build/how-to/subscribe-to-views/index.md new file mode 100644 index 0000000..9898a5e --- /dev/null +++ b/content/build/how-to/subscribe-to-views/index.md @@ -0,0 +1,6 @@ ++++ +title = "Subscribe to Views with the app-sdk" +description = "How to configure and start an embedded DefraDB instance, subscribe to Views, and receive pushed data in a Go application." ++++ + +This page is coming soon. diff --git a/content/build/how-to/use-shinzo-studio/index.md b/content/build/how-to/use-shinzo-studio/index.md new file mode 100644 index 0000000..02a69c0 --- /dev/null +++ b/content/build/how-to/use-shinzo-studio/index.md @@ -0,0 +1,6 @@ ++++ +title = "Create and deploy Views in Shinzo Studio" +description = "How to create, deploy, and query Views from your browser with Shinzo Studio, without installing the CLI." ++++ + +This page is coming soon. diff --git a/content/build/how-to/use-your-own-infrastructure/index.md b/content/build/how-to/use-your-own-infrastructure/index.md new file mode 100644 index 0000000..b54b88f --- /dev/null +++ b/content/build/how-to/use-your-own-infrastructure/index.md @@ -0,0 +1,6 @@ ++++ +title = "Use your own infrastructure" +description = "How to run your own Host as a private Direct Client for your app: the strongest privacy and control, at the cost of operating a node." ++++ + +This page is coming soon. diff --git a/content/build/how-to/verify-data/index.md b/content/build/how-to/verify-data/index.md new file mode 100644 index 0000000..6023562 --- /dev/null +++ b/content/build/how-to/verify-data/index.md @@ -0,0 +1,6 @@ ++++ +title = "Verify data with signatures and CIDs" +description = "How to verify who signed your data and navigate attestations, commits, and documents via CIDs." ++++ + +This page is coming soon. diff --git a/content/build/how-to/view-recipes/index.md b/content/build/how-to/view-recipes/index.md new file mode 100644 index 0000000..176b7db --- /dev/null +++ b/content/build/how-to/view-recipes/index.md @@ -0,0 +1,6 @@ ++++ +title = "View recipes" +description = "How to build common Views: decode event logs, filter by contract, decode multiple event types, transaction Views, and editing or rolling back a View." ++++ + +This page is coming soon. diff --git a/content/build/how-to/write-a-lens/index.md b/content/build/how-to/write-a-lens/index.md new file mode 100644 index 0000000..77bb403 --- /dev/null +++ b/content/build/how-to/write-a-lens/index.md @@ -0,0 +1,6 @@ ++++ +title = "Write and test a custom lens" +description = "How to author a WebAssembly lens with the AssemblyScript SDK, test it locally, and attach it to a View." ++++ + +This page is coming soon. diff --git a/content/build/index.md b/content/build/index.md new file mode 100644 index 0000000..3851fc7 --- /dev/null +++ b/content/build/index.md @@ -0,0 +1,6 @@ ++++ +title = "Build apps" +description = "Choose your path for building on Shinzo: query existing Views, create your own View, or build a local-first app." ++++ + +This page is coming soon. diff --git a/content/build/tutorials/build-a-local-first-app/index.md b/content/build/tutorials/build-a-local-first-app/index.md new file mode 100644 index 0000000..95c5878 --- /dev/null +++ b/content/build/tutorials/build-a-local-first-app/index.md @@ -0,0 +1,6 @@ ++++ +title = "Build a local-first app" +description = "Tutorial: build a Go app that embeds DefraDB via the app-sdk, subscribes to a View, and queries pushed data locally with attestation filtering." ++++ + +This page is coming soon. diff --git a/content/build/tutorials/create-your-first-view/index.md b/content/build/tutorials/create-your-first-view/index.md new file mode 100644 index 0000000..be223d0 --- /dev/null +++ b/content/build/tutorials/create-your-first-view/index.md @@ -0,0 +1,6 @@ ++++ +title = "Create your first View" +description = "Tutorial: build, test, and deploy a Shinzo View with Viewkit, from install to querying it on the public testnet." ++++ + +This page is coming soon. diff --git a/content/build/tutorials/query-your-first-view/index.md b/content/build/tutorials/query-your-first-view/index.md new file mode 100644 index 0000000..582f7f9 --- /dev/null +++ b/content/build/tutorials/query-your-first-view/index.md @@ -0,0 +1,6 @@ ++++ +title = "Query your first View" +description = "Tutorial: query a live View on the Shinzo testnet from a TypeScript app. Discover a View, pick a Host, sign a request, and print results." ++++ + +This page is coming soon. From 83e4d37033485c727d658e8e85793f15741b81af Mon Sep 17 00:00:00 2001 From: johnnymatthews <9611008+johnnymatthews@users.noreply.github.com> Date: Thu, 3 Sep 2026 11:29:31 +0000 Subject: [PATCH 04/11] Adds first draft pass. --- .../tutorials/query-your-first-view/index.md | 232 +++++++++++++++++- 1 file changed, 231 insertions(+), 1 deletion(-) diff --git a/content/build/tutorials/query-your-first-view/index.md b/content/build/tutorials/query-your-first-view/index.md index 582f7f9..6033f5f 100644 --- a/content/build/tutorials/query-your-first-view/index.md +++ b/content/build/tutorials/query-your-first-view/index.md @@ -3,4 +3,234 @@ title = "Query your first View" description = "Tutorial: query a live View on the Shinzo testnet from a TypeScript app. Discover a View, pick a Host, sign a request, and print results." +++ -This page is coming soon. +In this tutorial you'll query a live View on the Shinzo testnet from a small TypeScript script. By the end, the script prints the 10 most recent rows of `Erc20Event`, a registered View that decodes `Transfer` events from fungible token contracts on Ethereum mainnet. Everything runs against public testnet infrastructure; the only things you install on your machine are npm packages. + +The script does four things: find the View in ShinzoHub's registry, pick a Host that serves it, sign the GraphQL request, and POST it to the Host's endpoint. + +## Prerequisites + +- [Node.js](https://nodejs.org/) 20 or later. +- npm, which comes with Node.js. + +You don't need a wallet, tokens, or any local infrastructure. + +## Set up the project + +1. Check your Node.js version: + + ```shell + node --version + ``` + + ```output + v22.22.1 + ``` + +1. Create a project folder and install the dependencies: + + ```shell + mkdir query-view && cd query-view + npm init -y + npm pkg set type=module + npm install @shinzo/shinzohub @shinzo/querysig viem + npm install --save-dev tsx + ``` + + `@shinzo/shinzohub` reads the ShinzoHub registry (Views, Hosts, and pools). `@shinzo/querysig` builds the signed request envelope a Host expects. `viem` creates the signing key and produces signatures. `tsx` runs TypeScript files directly. The `npm pkg set` line marks the project as ESM so the script can use top-level `await`. + +## Connect to ShinzoHub + +1. Create a file called `query.ts` with the imports and a client: + + ```ts + import { createPublicClient, http } from "viem"; + import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; + import { shinzoHubActions } from "@shinzo/shinzohub"; + import { shinzoHubTestnet } from "@shinzo/shinzohub/chains"; + import { normalizeShinzoAddress, shinzoAddressToHex } from "@shinzo/shinzohub/addresses"; + import { sign } from "@shinzo/querysig"; + + const client = createPublicClient({ + chain: shinzoHubTestnet, + transport: http(), + }).extend(shinzoHubActions); + ``` + + `shinzoHubTestnet` carries the testnet chain ID (91273001) and its public endpoints. Extending the client with `shinzoHubActions` adds the registry read methods used below. + +## Find a View + +1. Add the discovery step to `query.ts`: + + ```ts + const { views } = await client.listViews({ limit: 25, includeMetadata: true }); + const view = views.find((v) => v.name === "Erc20Event"); + if (!view) { + throw new Error("Erc20Event is not registered"); + } + console.log(`View: ${view.name} at ${view.viewAddress}`); + console.log(`SDL: ${view.metadata?.sdl}`); + ``` + + `listViews` reads the View registry. Each entry has a `name`, a deterministic `viewAddress`, and, when `includeMetadata` is set, the parsed bundle: the source query, the SDL, and lens details. The script picks `Erc20Event` by name and prints its SDL, which lists the fields a query can ask for. + + You can also browse registered Views in the [Shinzo Explorer](https://explorer.shinzo.network/shinzohub). + +## Pick a Host + +A View is served by a pool of Hosts. `listViewPools` returns the pools that exist for a View, and a pool becomes active once at least 3 Hosts have joined it. Each registered Host advertises an `endpointAddress`, the full URL of its GraphQL API. + +1. Add the Host selection step: + + ```ts + const pools = await client.listViewPools({ viewAddress: view.viewAddress }); + const pool = pools.find((p) => p.isActive) ?? pools[0]; + if (!pool) { + throw new Error("No pool exists yet for this View"); + } + console.log(`Pool: ${pool.poolAddress} with ${pool.hosts.length} Hosts (active: ${pool.isActive})`); + + const { hosts } = await client.listHosts({ limit: 100 }); + const members = new Set(pool.hosts.map((h) => h.hostAddress.toLowerCase())); + const endpoints = hosts.flatMap((h) => { + if (!h.endpointAddress) return []; + try { + const hex = shinzoAddressToHex(normalizeShinzoAddress(h.address)).toLowerCase(); + return [{ endpoint: h.endpointAddress, inPool: members.has(hex) }]; + } catch { + return []; + } + }); + const candidates = [ + ...endpoints.filter((e) => e.inPool), + ...endpoints.filter((e) => !e.inPool), + ]; + ``` + + Pools track their members by EVM hex address, while the Host registry uses Shinzo bech32 addresses, so the script converts with `shinzoAddressToHex` before matching. The result is a candidate list with pool members first. + + {% admonition(type="note") %} + Registered endpoints can go stale on a testnet. The script tries pool members first, then falls back to any other registered Host that answers. Hosts replicate the Views they subscribe to, so the data is the same either way. + {% end %} + +## Sign the query + +Hosts expect every View query to carry a signature. The `sign` function from `@shinzo/querysig` hashes your query (canonical JSON plus keccak256), builds an EIP-712 `QueryRequest` over the query hash, a nonce, a timestamp, and the pool address, then asks your signer for a signature. Here the signer is a freshly generated key wrapped in viem's `signTypedData`; in a browser app the same call goes to the user's wallet. + +1. Add the query and the signing step: + + ```ts + const account = privateKeyToAccount(generatePrivateKey()); + const query = `query LatestEvents { + Erc20Event(filter: { event: { _eq: "Transfer" } }, limit: 100) { + blockNumber + event + logAddress + arguments + } + }`; + const signed = await sign( + { chainId: shinzoHubTestnet.id, pool: pool.poolAddress, query }, + (typedData) => account.signTypedData(typedData), + ); + console.log("Signed request extensions:"); + console.log(JSON.stringify(signed.extensions, null, 2)); + ``` + + The query asks for the block number, the event name, the contract that emitted the log (`logAddress`), and `arguments`, where the View's lens puts the decoded event parameters. The SDL also lists `hash`, `from`, and `to`, but this version of the View leaves those empty. + + The result is the query plus an `extensions` envelope: `request_signature` is the EIP-712 signature, `query_hash` binds the signature to this exact query, `nonce` and `request_timestamp` keep the request fresh, and `pool_address` names the pool the query bills to. `fanout` is only read by the network gateway; Hosts ignore it. + + {% admonition(type="note") %} + Query billing is not enforced on the testnet yet, so a freshly generated key with no funds is enough here. Signed requests are still the supported interface: once Hosts enforce billing, they will reject unsigned View queries and check the signer's query balance. + {% end %} + +## Send the query + +1. Add the send loop: + + ```ts + let answered = false; + for (const candidate of candidates) { + try { + const response = await fetch(candidate.endpoint, { + method: "POST", + headers: { "Content-Type": "application/json", Accept: "application/json" }, + body: JSON.stringify({ + query: signed.query, + variables: signed.variables, + extensions: signed.extensions, + }), + signal: AbortSignal.timeout(15_000), + }); + if (!response.ok) continue; + const body = await response.json(); + const rows = body.data?.Erc20Event; + if (!rows?.length) continue; + console.log(`Answer from ${candidate.endpoint}:`); + const latest = rows + .sort((a: { blockNumber: number }, b: { blockNumber: number }) => b.blockNumber - a.blockNumber) + .slice(0, 10); + for (const row of latest) { + const arg = (name: string) => + row.arguments?.find((a: { name: string }) => a.name === name)?.value ?? ""; + console.log(`${row.blockNumber} ${row.event} from=${arg("from")} to=${arg("to")} value=${arg("value")}`); + } + answered = true; + break; + } catch { + // This endpoint did not answer; try the next one. + } + } + if (!answered) { + throw new Error("No Host answered. Wait a minute and run the script again."); + } + ``` + + The request body is standard GraphQL-over-HTTP plus the `extensions` envelope. The script POSTs to each candidate until one answers, sorts the returned rows by `blockNumber`, and prints the 10 most recent. Sorting locally keeps the request to one bounded page; [Query data](/build/query-data/) covers the server-side `filter` and `order` arguments if you want the Host to do that work. + +## Run the script + +1. Run it: + + ```shell + npx tsx query.ts + ``` + + ```output + View: Erc20Event at 0xEAc245f905e0aAcF3b9Fe27153F2AaF485dc1B48 + SDL: type Erc20Event @materialized(if: false) { hash: String blockNumber: Int from: String to: String logAddress: String event: String signature: String arguments: String } + Pool: 0xDbc3bE7CBd8Dc8901E3BbbeA1A740BE490dAe23B with 3 Hosts (active: true) + Signed request extensions: + { + "request_signature": "0x7af2957e06e2a8077b881b18bb2d00b2adce8a5f8ebed78f15f5f85ac1684e5a5e2443384d353efac483e3c15dc45d9f2f9c72559c820b5a5a697aaa18521a611c", + "nonce": "0xfa7ee71bd179843e04944a083c999ba32805f758cdae2699f604c9c9928c2219", + "query_hash": "0xfa3ec6d0ff7e89a1bea76e1042b12d4e89366f13efaf935217f7331c8a44a8c2", + "request_timestamp": 1788433155, + "pool_address": "0xDbc3bE7CBd8Dc8901E3BbbeA1A740BE490dAe23B", + "fanout": 1 + } + Answer from http://51.178.74.112:9181/api/v0/graphql: + 25806062 Transfer from=0xcdb71d4c6b3a0d470201f848e50c7411a521ee04 to=0xc1d13492285eb664951e201bf7c80c7c6318a1b5 value=2000000000 + 25806062 Transfer from=0xbbbbbbbbbb9cc5e90e3b3af64bdaf62c37eeffcb to=0xd226997439ecfbeff8e110c8c78c8a7eefd19f89 value=102876390302159 + 25806062 Transfer from=0xd226997439ecfbeff8e110c8c78c8a7eefd19f89 to=0xdcef968d416a41cdac0ed8702fac8128a64241a2 value=421000000 + 25806062 Transfer from=0x09fc9b7545020f6a51d113e495e0a451597969d3 to=0xf8e349d1d827a6edf17ee673664cfad4ca78c533 value=374880000 + 25806062 Transfer from=0x9642b23ed1e01df1092b92641051881a322f5d4e to=0x89df61e9ae683899d376ed60964d9e8c3fb27160 value=25265642000 + 25806062 Transfer from=0x22ec88b9ff78c6f2458ab1a7aa8bb99d84bd4b86 to=0x003896387666c5c11458eeb3f927b72a11b19783 value=2212522960 + 25805853 Transfer from=0x3416cf6c708da44db2624d63ea0aaef7113527c6 to=0xace0fabed501e819ecc15e67c7ed3a67c2f67e91 value=629288918 + 25805853 Transfer from=0xace0fabed501e819ecc15e67c7ed3a67c2f67e91 to=0x2c158bc456e027b2affccadf1bdbd9f5fc4c5c8c value=628725538 + 25805853 Transfer from=0x2c158bc456e027b2affccadf1bdbd9f5fc4c5c8c to=0x3312cc371fe0dd5171878630a1e5cf69778e8fa5 value=628725538 + 25805153 Transfer from=0xe0554a476a092703abdb3ef35c80e0d76d32939f to=0xa0f1c3ad83e07d97b5e7030e177718be175275ea value=3741969400 + ``` + + Registered Hosts, pool membership, and the rows themselves change as the network moves, so your addresses and values will differ. + +## Where to next + +- [Create your first View](/build/tutorials/create-your-first-view/) to own the data: build and deploy a View of your own, then query it with this same script. +- [Build a local-first app](/build/tutorials/build-a-local-first-app/) for the embedded version: subscribe to a View and query pushed data locally instead of per request. +- [Choosing an app architecture](/build/explanation/choosing-an-architecture/) for the trade-offs between the two models. + +## Need help + +{{ need_help(client="Shinzo SDK", repo_name="web", repo="https://github.com/shinzonetwork/web/issues") }} From aaeafbdf59ae8ba99a97c967b710557c571e530b Mon Sep 17 00:00:00 2001 From: johnnymatthews <9611008+johnnymatthews@users.noreply.github.com> Date: Thu, 3 Sep 2026 13:07:16 +0100 Subject: [PATCH 05/11] Tiny copy edits. --- content/build/tutorials/query-your-first-view/index.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/content/build/tutorials/query-your-first-view/index.md b/content/build/tutorials/query-your-first-view/index.md index 6033f5f..c2b7d62 100644 --- a/content/build/tutorials/query-your-first-view/index.md +++ b/content/build/tutorials/query-your-first-view/index.md @@ -5,12 +5,17 @@ description = "Tutorial: query a live View on the Shinzo testnet from a TypeScri In this tutorial you'll query a live View on the Shinzo testnet from a small TypeScript script. By the end, the script prints the 10 most recent rows of `Erc20Event`, a registered View that decodes `Transfer` events from fungible token contracts on Ethereum mainnet. Everything runs against public testnet infrastructure; the only things you install on your machine are npm packages. -The script does four things: find the View in ShinzoHub's registry, pick a Host that serves it, sign the GraphQL request, and POST it to the Host's endpoint. +The script we're about to create does four things: + +1. Find the View in ShinzoHub's registry. +1. Pick a Host that serves it. +1. Sign the GraphQL request. +1. POST it to the Host's endpoint. ## Prerequisites - [Node.js](https://nodejs.org/) 20 or later. -- npm, which comes with Node.js. +- NPM, which comes with Node.js anyway. You don't need a wallet, tokens, or any local infrastructure. From 5c1f9d27d88c2143c7a0723fbc7c2902a816ff4a Mon Sep 17 00:00:00 2001 From: johnnymatthews <9611008+johnnymatthews@users.noreply.github.com> Date: Fri, 4 Sep 2026 11:42:36 +0100 Subject: [PATCH 06/11] Adds local-devnet guide. --- config.toml | 1 + .../tutorials/spinup-a-local-devnet/index.md | 424 ++++++++++++++++++ 2 files changed, 425 insertions(+) create mode 100644 content/build/tutorials/spinup-a-local-devnet/index.md diff --git a/config.toml b/config.toml index ffa5240..30596a5 100644 --- a/config.toml +++ b/config.toml @@ -131,6 +131,7 @@ pages = [ "build/tutorials/query-your-first-view/index.md", "build/tutorials/create-your-first-view/index.md", "build/tutorials/build-a-local-first-app/index.md", + "build/tutorials/spinup-a-local-devnet/index.md", ] [[extra.sidebar]] diff --git a/content/build/tutorials/spinup-a-local-devnet/index.md b/content/build/tutorials/spinup-a-local-devnet/index.md new file mode 100644 index 0000000..53316fb --- /dev/null +++ b/content/build/tutorials/spinup-a-local-devnet/index.md @@ -0,0 +1,424 @@ ++++ +title = "Spinup a local devnet" +description = "Tutorial: start a devnet locally on your machine to test basic Shinzo and blockchain functionality." ++++ + +This tutorial walks through spinning up a local Ethereum development network using Geth, deploying an ERC-20 smart contract to it, and connecting the Shinzo Generator client and Host so blockchain data is generated into DefraDB and queryable via GraphQL. + +## How it fits together + +Here's the basic flow for what we're about to build: + +```plaintext +Geth (--dev) → mines blocks on demand + ↓ +Shinzo Generator client → watches for new blocks via HTTP/WS RPC + ↓ +DefraDB (embedded in Generator client) → stores generated block/transaction/log data + ↓ (P2P passive replication) +DefraDB (embedded in Host) → receives replicated data + ↓ +GraphQL API (port 9182) → query the replicated data +``` + +### Prerequisites + +- A cloud VM running Ubuntu 24.04 on amd64 (e.g. Hetzner CX22 or similar). arm64 won't work as the Generator and Host images are amd64-only. +- A GitHub account with access to the Shinzo container registry. + +## Geth + +### Install Geth + +Add the official Ethereum PPA and install Geth: + +```shell +sudo add-apt-repository -y ppa:ethereum/ethereum +sudo apt install -y geth +``` + +Verify the install: + +```shell +geth version +``` + +### Start Geth in dev mode + +```shell +geth --dev \ + --http \ + --http.addr 0.0.0.0 \ + --http.api eth,net,web3 \ + --http.vhosts='*' \ + --ws \ + --ws.addr 0.0.0.0 \ + --ws.api eth,net,web3 \ + --verbosity 3 +``` + +Things to note from the output: +- Geth logs a pre-funded developer account address and private key. Save these; you'll need them later. +- HTTP RPC listens on `0.0.0.0:8545`. +- WebSocket listens on `0.0.0.0:8546`. +- The chain only mines a block when a transaction is pending, so it won't produce empty blocks. + +### Verify Geth is running + +In a second terminal, check the chain is responding: + +```shell +curl -s -X POST http://localhost:8545 \ + -H "Content-Type: application/json" \ + --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' +``` + +Expected response: `{"jsonrpc":"2.0","id":1,"result":"0x0"}` + +`0x0` means the chain is at block zero. That's correct for a fresh devnet. + +### Attach the Geth console and test + +Attach to the running node: + +```shell +geth attach http://localhost:8545 +``` + +Check your dev account and balance: + +```javascript +eth.accounts +eth.getBalance(eth.accounts[0]) +``` + +The balance will be an enormous number. That's the pre-funded dev account balance in wei, essentially unlimited play money. + +Send a test transaction to trigger block mining: + +```javascript +eth.sendTransaction({from: eth.accounts[0], to: eth.accounts[0], value: 1}) +``` + +Confirm the block number advanced: + +```javascript +eth.blockNumber +``` + +## Generator client + +### Install Docker + +Install Docker and add your user to the docker group. Verify with: + +```shell +docker --version +``` + +### Authenticate with GitHub Container Registry + +Generate a GitHub Personal Access Token with the `read:packages` scope, then log in: + +```shell +echo YOUR_PAT | docker login ghcr.io -u YOUR_GITHUB_USERNAME --password-stdin +``` + +### Pull the Shinzo Generator image + +Images are published per chain rather than as a single `standard` tag. For Ethereum: + +```shell +docker pull ghcr.io/shinzonetwork/shinzo-generator-client:ethereum-mainnet-latest +``` + +### Run the Generator client + +Get your VM's public IP: + +```shell +hostname -I +``` + +Use the first IP in the output. Then run the Generator client, pointing it at your Geth node. Three ports are exposed: `9181` (DefraDB GraphQL API), `9171` (libp2p, for the Host to connect over), and `8080` (health/metrics/registration): + +```shell +docker run --rm \ + -e GETH_RPC_URL=http://YOUR_VM_IP:8545 \ + -e GETH_WS_URL=ws://YOUR_VM_IP:8546 \ + -e GETH_API_KEY="" \ + -e INDEXER_START_HEIGHT=0 \ + -e DEFRADB_KEYRING_SECRET=devnet-secret \ + -e DEFRADB_P2P_ENABLED=true \ + -e DEFRADB_P2P_LISTEN_ADDR=/ip4/0.0.0.0/tcp/9171 \ + -e LOGGER_DEBUG=true \ + -p 9181:9181 \ + -p 9171:9171 \ + -p 8080:8080 \ + ghcr.io/shinzonetwork/shinzo-generator-client:ethereum-mainnet-latest +``` + +`INDEXER_START_HEIGHT` keeps its old name deliberately: like the `GETH_*` variables, it's a historical env var name that wasn't renamed when the client itself became the "Generator client." `DEFRADB_P2P_LISTEN_ADDR` tells DefraDB which interface and port to bind libp2p to inside the container; binding to `0.0.0.0:9171` means the Host container, on the same Docker bridge, can reach it. + +You should see the Generator client connect to Geth, generate block 0, and then wait for new blocks. + +### Get the Generator client's Peer ID + +You'll need this to connect the Host. The Peer ID and dialable address both come from the Generator client's `/health` endpoint once it's finished starting up: + +```shell +curl -s http://localhost:8080/health | jq '.p2p.self' +``` + +```output +{ + "id": "12D3KooWK8zmiDmX91PwDV1PsqtgA1UUDuuyipVBVPEjrvwgoFJH", + "addresses": [ + "/ip4/127.0.0.1/tcp/9171", + "/ip4/172.17.0.2/tcp/9171" + ], + "public_key": "8a7f061eeaaec8b8130ce4b9d6e519bbe76b9a4bc038b7e6743a773ad3915e02" +} +``` + +`id` is the Generator client's libp2p Peer ID, stable across restarts as long as the keyring secret stays the same. Of the `addresses`, ignore the `127.0.0.1` one. It's loopback and useless to other containers. Use the other one (the Generator client container's IP on the Docker bridge), which is what the Host will dial. + +Capture both into shell variables: + +```shell +PEER_ID=$(curl -s http://localhost:8080/health | jq -r '.p2p.self.id') + +GENERATOR_IP=$(curl -s http://localhost:8080/health \ + | jq -r '[.p2p.self.addresses[] | capture("/ip4/(?[0-9.]+)/").ip + | select(. != "127.0.0.1" and . != "0.0.0.0")][0]') + +BOOTSTRAP_PEER="/ip4/${GENERATOR_IP}/tcp/9171/p2p/${PEER_ID}" +echo "$BOOTSTRAP_PEER" +``` + +### Verify data in DefraDB + +First check what query fields are available: + +```shell +curl -s -X POST http://localhost:9181/api/v0/graphql \ + -H "Content-Type: application/json" \ + --data '{"query":"{ __schema { queryType { fields { name } } } }"}' +``` + +Then query the generated blocks: + +```shell +curl -s -X POST http://localhost:9181/api/v0/graphql \ + -H "Content-Type: application/json" \ + --data '{"query":"{ Ethereum__Mainnet__Block { hash number } }"}' +``` + +You should see block 0 and block 1 returned with their hashes. + +## Smart contract + +### Install Foundry + +Foundry provides the `forge` and `cast` CLI tools for compiling and deploying smart contracts. + +```shell +curl -L https://foundry.paradigm.xyz | bash +source ~/.bashrc +``` + +Verify the install: + +```shell +forge --version +``` + +### Deploy an ERC-20 token contract + +Create a new Foundry project: + +```shell +forge init shinzo-token && cd shinzo-token +``` + +Install the OpenZeppelin contracts library: + +```shell +forge install OpenZeppelin/openzeppelin-contracts +``` + +Create the token contract: + +```shell +cat > src/ShinzoToken.sol << 'EOF' +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract ShinzoToken is ERC20 { + constructor() ERC20("ShinzoToken", "SHINZO") { + _mint(msg.sender, 1000000 * 10 ** decimals()); + } +} +EOF +``` + +Set up the OpenZeppelin import remapping: + +```shell +cat > remappings.txt << 'EOF' +@openzeppelin/=lib/openzeppelin-contracts/ +EOF +``` + +Compile the contract: + +```shell +forge build +``` + +Deploy it to the devnet, replacing `YOUR_PRIVATE_KEY` with the key Geth logged on startup: + +```shell +forge create src/ShinzoToken.sol:ShinzoToken \ + --rpc-url http://localhost:8545 \ + --private-key YOUR_PRIVATE_KEY \ + --broadcast +``` + +Note down the `Deployed to` address from the output; you'll need it in the next step. + +### Send a token transfer + +Use `cast` to call the `transfer` function on the deployed contract. This emits a `Transfer` event which the Generator client will capture: + +```shell +cast send YOUR_CONTRACT_ADDRESS \ + "transfer(address,uint256)" \ + YOUR_DEV_ACCOUNT_ADDRESS \ + 1000 \ + --rpc-url http://localhost:8545 \ + --private-key YOUR_PRIVATE_KEY +``` + +Watch the Generator client terminal. You should see it pick up the new block and commit it to DefraDB. + +### Query the generated data + +Query all generated transactions: + +```shell +curl -s -X POST http://localhost:9181/api/v0/graphql \ + -H "Content-Type: application/json" \ + --data '{"query":"{ Ethereum__Mainnet__Transaction { hash blockNumber } }"}' +``` + +Query the generated logs to see the Transfer events: + +```shell +curl -s -X POST http://localhost:9181/api/v0/graphql \ + -H "Content-Type: application/json" \ + --data '{"query":"{ Ethereum__Mainnet__Log { address topics blockNumber } }"}' +``` + +You should see two `Transfer` events: one from the contract deployment (tokens minted from the zero address to your dev account) and one from the token transfer. The first topic on both (`0xddf252ad...`) is the keccak256 hash of `Transfer(address,address,uint256)`, the standard ERC-20 Transfer event signature, identical across every Ethereum network. + +## Host + +### Pull the Shinzo Host image + +Like the Generator, Host images are published per chain: + +```shell +docker pull ghcr.io/shinzonetwork/shinzo-host-client:ethereum-mainnet-latest +``` + +### Create the Host config + +The Host's config format has been simplified: most of the manual tuning fields (batch processing, pruner, snapshot) are no longer part of the base setup. Create the config file: + +```shell +cat > ~/host-config.yaml << 'EOF' +defradb: + url: "localhost:9181" + keyring_secret: "host-devnet-secret" + p2p: + enabled: true + bootstrap_peers: + - '${BOOTSTRAP_PEER}' + listen_addr: "/ip4/0.0.0.0/tcp/9171" + enable_auto_reconnect: true + store: + path: "./.defra" +shinzo: + hub_base_url: testnet.shinzo.network:26657 + minimum_attestations: 1 + start_height: 0 +logger: + development: true + level: "info" +host: + lens_registry_path: "./.defra/lens" + health_server_port: 8080 +EOF +``` + +A few notes on this: + +- `defradb.url: localhost:9181` is the Host's _internal_ DefraDB API, not the Generator's. Inside the Host container, DefraDB binds to `9181` on `localhost`. The Generator's API happens to use the same port number because they're both DefraDB; the published ports are remapped in the next step so they don't collide on the host machine. +- `bootstrap_peers` uses the `$BOOTSTRAP_PEER` value you built in the Generator section above. The Host learns everything else (schemas, signed data) from the Generator over P2P once it connects. +- `minimum_attestations: 1` means the Host will serve data as soon as it has one signature on it. Production setups use higher values to require independent confirmation from multiple Generator clients. +- `hub_base_url` points at ShinzoHub's testnet endpoint. This devnet setup isn't registering with the Hub, but the Host expects the field to be present. + +### Run the Host + +The Generator is already using `9181`, `9171`, and `8080` on the host machine, so the Host container's ports get bumped by one: + +```shell +docker run -d \ + --name shinzo-host \ + -e BOOTSTRAP_PEERS="$BOOTSTRAP_PEER" \ + -v ~/host-config.yaml:/app/config.yaml:ro \ + -p 9182:9181 \ + -p 9172:9171 \ + -p 8081:8080 \ + ghcr.io/shinzonetwork/shinzo-host-client:ethereum-mainnet-latest +``` + +`BOOTSTRAP_PEERS` is an override; the same value is already in the config file, but some Host builds also read the env var, so it's set in both places. + +### Verify the peering + +Check the Host's `/health` for the Generator's Peer ID: + +```shell +curl -s http://localhost:8081/health | jq '{status, current_block, p2p: {self: .p2p.self.id, peers: [.p2p.peers[].id]}}' +``` + +The `peers` array should contain the `PEER_ID` value from the Generator section. You can cross-check from the Generator's side too; its peer list should now contain the Host: + +```shell +curl -s http://localhost:8080/health | jq '[.p2p.peers[].id]' +``` + +If both list each other, libp2p is connected and DefraDB is replicating between them. + +### Verify data is replicating + +Send a transaction from the Geth console to produce a new block: + +```javascript +eth.sendTransaction({from: eth.accounts[0], to: eth.accounts[0], value: 1}) +``` + +Once `current_block` is non-zero on the Host's `/health`, query its GraphQL API on the remapped port `9182`: + +```shell +curl -s -X POST http://localhost:9182/api/v0/graphql \ + -H "Content-Type: application/json" \ + --data '{"query":"{ Ethereum__Mainnet__Block { hash number } }"}' | jq +``` + +You should see the new block returned. Passive replication only syncs new documents created after the P2P connection was established, so historical blocks won't appear in the Host's DefraDB. + From c8ef014e4e20dc049d7a6bdec440bc9bdb0997f5 Mon Sep 17 00:00:00 2001 From: johnnymatthews <9611008+johnnymatthews@users.noreply.github.com> Date: Fri, 4 Sep 2026 22:19:50 +0000 Subject: [PATCH 07/11] First pass at how-to section. Needs cleanup. --- content/build/create-a-view/examples/index.md | 623 ----------------- .../configure-attestation-thresholds/index.md | 95 ++- .../build/how-to/connect-to-a-host/index.md | 85 ++- .../how-to/find-views-and-hosts/index.md | 120 +++- content/build/how-to/query-data/index.md | 213 +++++- .../build/how-to/subscribe-to-views/index.md | 155 ++++- .../build/how-to/use-shinzo-studio/index.md | 47 +- .../use-your-own-infrastructure/index.md | 31 +- content/build/how-to/verify-data/index.md | 141 +++- content/build/how-to/view-recipes/index.md | 625 +++++++++++++++++- content/build/how-to/write-a-lens/index.md | 176 ++++- content/build/query-data/index.md | 347 ---------- content/reference/components/viewkit/index.md | 11 +- 13 files changed, 1682 insertions(+), 987 deletions(-) delete mode 100644 content/build/create-a-view/examples/index.md delete mode 100644 content/build/query-data/index.md diff --git a/content/build/create-a-view/examples/index.md b/content/build/create-a-view/examples/index.md deleted file mode 100644 index 89f73bc..0000000 --- a/content/build/create-a-view/examples/index.md +++ /dev/null @@ -1,623 +0,0 @@ -+++ -title = "View examples" -aliases = ["/views/examples"] -description = "Copy-pasteable Shinzo View examples: decode event logs, filter by contract, decode multiple event types, edit and update views, and query the results." -+++ - -Seven progressively more complex Views, from a basic event decode to a multi-event decoder with editing and updates. Each example shows the goal, the three View components (query, SDL, lens), the Viewkit commands to build it, and the GraphQL query you run against the result. - -## Primitive data - -Views query the primitive collections that Generator clients produce. All collection names are prefixed with `____`, derived from the Generator's `chain.name` and `chain.network` settings. Viewkit lets you use short names like `Log`, and the Host client auto-prefixes them at runtime. - -| Collection | Common fields | Typical use | -| --- | --- | --- | -| `Log` | `address`, `topics`, `data`, `transactionHash`, `blockNumber` | Event decoding (ERC-20, NFT, governance) | -| `Transaction` | `hash`, `from`, `to`, `value`, `blockNumber`, `status`, `gasUsed` | Transaction analytics | -| `Block` | `number`, `hash`, `timestamp`, `miner`, `gasUsed`, `gasLimit` | Block-level metadata | -| `AccessListEntry` | `address`, `storageKeys`, `blockNumber` | EIP-2930 access lists | - -There is no `Event` collection. Raw event data lives in `Log`, where `topics` holds indexed parameters and `data` holds non-indexed ones. A lens decodes those raw fields into structured output. For the full list of primitive collections, including `BlockSignature` and `SnapshotSignature`, see [Views for builders](/build/concepts/views-for-builders/#primitive-data-views-operate-on). - -## Decode event logs - -Decode all ERC-20 `Transfer` events into structured records. This is the simplest useful View that includes a lens: it decodes raw log `topics` and `data` into named fields using an ABI. - -### Query - -```graphql -Log { address topics data transactionHash blockNumber transaction { hash from to } } -``` - -The query selects raw log fields plus the nested `transaction` relation. The `decode_log` lens uses `transaction.hash`, `transaction.from`, and `transaction.to` to populate the output's `hash`, `from`, and `to` fields. - -### SDL - -```graphql -type EventView @materialized(if: true) { - hash: String - from: String - to: String - blockNumber: Int - logAddress: String - event: String - signature: String - arguments: [String] -} -``` - -The `decode_log` lens outputs these fields: - -- `hash`, `from`, `to`: from the parent transaction. -- `blockNumber`: block the log was emitted in. -- `logAddress`: the contract that emitted the log. -- `event`: decoded event name (e.g. `"Transfer"`). -- `signature`: event signature (e.g. `"Transfer(address,address,uint256)"`). -- `arguments`: array of decoded parameters as JSON strings. - -`@materialized(if: true)` tells DefraDB to pre-compute and store the output. See [Materialized versus on-query](#materialized-vs-on-query) for the tradeoff. - -### Lens - -| Lens | Purpose | Arguments | -| --- | --- | --- | -| `decode_log` | ABI-decode log events into named fields | `{"abi":"[{\"type\":\"event\",\"name\":\"Transfer\",...}]"}` | - -The `decode_log` lens takes an `abi` argument: a stringified JSON array of event definitions. For the ERC-20 `Transfer` event: - -```json -[{"type":"event","name":"Transfer","inputs":[{"type":"address","name":"from","indexed":true},{"type":"address","name":"to","indexed":true},{"type":"uint256","name":"value","indexed":false}]}] -``` - -### Commands - -1. Initialize the view: - - ```shell - viewkit view init event-view - ``` - -1. Add the query (raw log shape with transaction relation): - - ```shell - viewkit view add query \ - "Log { address topics data transactionHash blockNumber transaction { hash from to } }" \ - --name event-view - ``` - -1. Add the SDL (output schema matching decode_log output): - - ```shell - viewkit view add sdl \ - "type EventView @materialized(if: true) { hash: String from: String to: String blockNumber: Int logAddress: String event: String signature: String arguments: [String] }" \ - --name event-view - ``` - -1. Attach the decode lens with the Transfer ABI: - - ```shell - viewkit view add lens \ - --label "decode-transfer" \ - --url "https://raw.githubusercontent.com/shinzonetwork/wasm-bucket/main/bucket/decode_log/decode_log.wasm" \ - --args '{"abi":"[{\"type\":\"event\",\"name\":\"Transfer\",\"inputs\":[{\"type\":\"address\",\"name\":\"from\",\"indexed\":true},{\"type\":\"address\",\"name\":\"to\",\"indexed\":true},{\"type\":\"uint256\",\"name\":\"value\",\"indexed\":false}]}]"}' \ - --name event-view - ``` - -1. Inspect to confirm everything is attached: - -```shell -viewkit view inspect event-view -``` - -1. Test locally (optional but recommended): - -```shell -viewkit view test event-view -``` - -1. Deploy locally and explore in the playground: - -```shell -viewkit view deploy event-view --target local -``` - -### Querying the result - -Once deployed, open the DefraDB Playground (URL printed in the terminal) and run: - -```graphql -{ - EventView(limit: 10, order: { blockNumber: DESC }) { - hash - from - to - blockNumber - logAddress - event - signature - arguments - } -} -``` - -This returns all decoded `Transfer` events across all contracts. To narrow down to a specific token, see [Example 2](#filter-by-contract-address). - -## Filter by contract address - -Decode `Transfer` events from a specific contract only (e.g. USDC). Without a filter lens, `decode_log` processes every log on the chain. You filter the output using GraphQL queries against the `logAddress` field. - -### Query and SDL - -Same as the [Decode event logs example](#decode-event-logs). The query, SDL, and lens are identical. The filtering happens at query time, not at the lens level. - -### Commands - -Same as Example 1. Create a view named `usdc-event` with the same query, SDL, and lens. - -### USDC transfers only - -```graphql -{ - EventView( - filter: { logAddress: { _eq: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } } - order: { blockNumber: DESC } - limit: 10 - ) { - hash - from - to - blockNumber - event - signature - arguments - } -} -``` - -The `logAddress` field contains the contract address that emitted the log. Filter on it to narrow results to one contract. - -### Filter by sender or receiver - -The `from` and `to` fields come from the parent transaction, not the event's indexed parameters. To filter by the event's `from`/`to` (the actual transfer sender and receiver), use the `arguments` field. With `decode_log_str` (which serializes `arguments` as a JSON string), you can use `_like`: - -```graphql -{ - EventView( - filter: { - _and: [ - { logAddress: { _eq: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } } - { arguments: { _like: "%0x28C6c06298d514De0879A2640AB71F86b50Ce4E5%" } } - ] - } - ) { - hash - from - to - arguments - blockNumber - } -} -``` - -{% admonition(type="tip", title="decode_log vs decode_log_str") %} -`decode_log` outputs `arguments` as a JSON array (`[String]` in SDL). `decode_log_str` outputs it as a JSON string (`String` in SDL), which enables `_like` filtering in DefraDB queries. Use `decode_log_str` when you need to filter on decoded parameter values. The `_str` variant uses the same URL pattern but with `decode_log_str` in the path. -{% end %} - -## Decode multiple event types - -Decode both `Transfer` and `Approval` events from a single contract in one View. Pass both event definitions in the ABI argument to `decode_log`. - -### Query - -```graphql -Log { address topics data transactionHash blockNumber transaction { hash from to } } -``` - -### SDL - -```graphql -type EventView @materialized(if: true) { - hash: String - from: String - to: String - blockNumber: Int - logAddress: String - event: String - signature: String - arguments: [String] -} -``` - -### Lens - -The ABI argument includes both `Transfer` and `Approval` event definitions. The `decode_log` lens matches each log's `topics[0]` against the event signature hash and decodes accordingly. - -```json -[ - {"type":"event","name":"Transfer","inputs":[{"type":"address","name":"from","indexed":true},{"type":"address","name":"to","indexed":true},{"type":"uint256","name":"value","indexed":false}]}, - {"type":"event","name":"Approval","inputs":[{"type":"address","name":"owner","indexed":true},{"type":"address","name":"spender","indexed":true},{"type":"uint256","name":"value","indexed":false}]} -] -``` - -### Commands - -```shell -# 1) initialize the view -viewkit view init erc20-events - -# 2) add the query -viewkit view add query \ - "Log { address topics data transactionHash blockNumber transaction { hash from to } }" \ - --name erc20-events - -# 3) add the SDL -viewkit view add sdl \ - "type EventView @materialized(if: true) { hash: String from: String to: String blockNumber: Int logAddress: String event: String signature: String arguments: [String] }" \ - --name erc20-events - -# 4) attach the decode lens with both Transfer and Approval in the ABI -viewkit view add lens \ - --label "decode-erc20" \ - --url "https://raw.githubusercontent.com/shinzonetwork/wasm-bucket/main/bucket/decode_log/decode_log.wasm" \ - --args '{"abi":"[{\"type\":\"event\",\"name\":\"Transfer\",\"inputs\":[{\"type\":\"address\",\"name\":\"from\",\"indexed\":true},{\"type\":\"address\",\"name\":\"to\",\"indexed\":true},{\"type\":\"uint256\",\"name\":\"value\",\"indexed\":false}]},{\"type\":\"event\",\"name\":\"Approval\",\"inputs\":[{\"type\":\"address\",\"name\":\"owner\",\"indexed\":true},{\"type\":\"address\",\"name\":\"spender\",\"indexed\":true},{\"type\":\"uint256\",\"name\":\"value\",\"indexed\":false}]}]"}' \ - --name erc20-events - -# 5) inspect, test, and deploy -viewkit view inspect erc20-events -viewkit view test erc20-events -viewkit view deploy erc20-events --target local -``` - -### Transfers only - -```graphql -{ - EventView( - filter: { - _and: [ - { logAddress: { _eq: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } } - { event: { _eq: "Transfer" } } - ] - } - limit: 10 - ) { - hash - from - to - event - arguments - blockNumber - } -} -``` - -### Approvals only - -```graphql -{ - EventView( - filter: { - _and: [ - { logAddress: { _eq: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } } - { event: { _eq: "Approval" } } - ] - } - limit: 10 - ) { - hash - from - to - event - arguments - blockNumber - } -} -``` - -The `event` field lets you distinguish between event types in the same View collection. - -## Transaction-based view (no lens) - -Expose all transactions sent to a specific contract. This View queries `Transaction` documents directly. No lens needed because we're not decoding events. - -### Query - -```graphql -Transaction { hash from to value blockNumber gasUsed gasPrice } -``` - -### SDL - -```graphql -type TransactionView @materialized(if: false) { - hash: String - from: String - to: String - value: String - blockNumber: Int - gasUsed: String - gasPrice: String -} -``` - -Here we use `@materialized(if: false)`: the view is computed on query, not pre-stored. This makes sense for transaction data, which is large and queried less frequently than decoded events. See [Example 5](#materialized-vs-on-query) for details. - -### Lens - -None. The query and SDL are sufficient. DefraDB applies the view as a virtual projection over the `Transaction` collection. - -### Commands - -```shell -# 1) initialize the view -viewkit view init transaction-view - -# 2) add the query -viewkit view add query \ - "Transaction { hash from to value blockNumber gasUsed gasPrice }" \ - --name transaction-view - -# 3) add the SDL -viewkit view add sdl \ - "type TransactionView @materialized(if: false) { hash: String from: String to: String value: String blockNumber: Int gasUsed: String gasPrice: String }" \ - --name transaction-view - -# 4) inspect, test, and deploy -viewkit view inspect transaction-view -viewkit view test transaction-view -viewkit view deploy transaction-view --target local -``` - -### Querying the result - -```graphql -{ - TransactionView( - filter: { to: { _eq: "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45" } } - order: { blockNumber: DESC } - limit: 10 - ) { - hash - from - to - value - blockNumber - gasUsed - gasPrice - } -} -``` - -## Materialized vs on-query - -Understand when to use `@materialized(if: true)` vs `@materialized(if: false)`. - -The `@materialized` directive controls when the View's output is computed: - -| `@materialized` | When it computes | Query speed | Storage | Best for | -| --- | --- | --- | --- | --- | -| `if: true` | At write time. Host pre-computes and stores results. | Fast. Data is already materialized. | Higher. Host stores the output collection. | Frequently queried data (e.g. token transfers in a UI). | -| `if: false` | At query time. Host computes on the fly. | Slower. Depends on data volume. | Lower. No pre-stored output. | Large datasets queried occasionally, or during development. | - -### Same view in two modes - -Materialized (pre-computed): - -```graphql -type EventView @materialized(if: true) { - hash: String - from: String - to: String - blockNumber: Int - logAddress: String - event: String - signature: String - arguments: [String] -} -``` - -On-query (virtual): - -```graphql -type EventView @materialized(if: false) { - hash: String - from: String - to: String - blockNumber: Int - logAddress: String - event: String - signature: String - arguments: [String] -} -``` - -### Switching modes - -To toggle materialization on an existing view, update the SDL: - -```shell -# switch to materialized -viewkit view add sdl \ - "type EventView @materialized(if: true) { hash: String from: String to: String blockNumber: Int logAddress: String event: String signature: String arguments: [String] }" \ - --name event-view - -# or switch to on-query -viewkit view add sdl \ - "type EventView @materialized(if: false) { hash: String from: String to: String blockNumber: Int logAddress: String event: String signature: String arguments: [String] }" \ - --name event-view -``` - -Then redeploy: - -```shell -viewkit view test event-view -viewkit view deploy event-view --target local -``` - -{% admonition(type="tip") %} -Use `@materialized(if: false)` while developing and iterating on a View. Switch to `@materialized(if: true)` once the View is stable and you need fast queries in production. -{% end %} - -## Editing and updating a view - -Modify an existing View without starting from scratch. This example builds on the `erc20-events` View from [Example 3](#decode-multiple-event-types) and shows the full edit-update lifecycle: add an SDL field, swap a lens, inspect revisions, roll back, test, and redeploy. - -### Starting point - -Assume you already have `erc20-events` deployed with: - -- Query: `Log { address topics data transactionHash blockNumber transaction { hash from to } }` -- SDL: `type EventView @materialized(if: true) { hash: String from: String to: String blockNumber: Int logAddress: String event: String signature: String arguments: [String] }` -- Lens: `decode-erc20` (decode_log, Transfer + Approval ABI) - -### Swap the lens to decode three event types - -Remove the old lens, then add a new one with an updated ABI that includes `Transfer`, `Approval`, and `Transfer` (ERC-721, which has a `tokenId` instead of `value`): - -```shell -# remove the old lens -viewkit view remove lens \ - --label "decode-erc20" \ - --name erc20-events - -# add a new lens with three event types -viewkit view add lens \ - --label "decode-multi" \ - --url "https://raw.githubusercontent.com/shinzonetwork/wasm-bucket/main/bucket/decode_log/decode_log.wasm" \ - --args '{"abi":"[{\"type\":\"event\",\"name\":\"Transfer\",\"inputs\":[{\"type\":\"address\",\"name\":\"from\",\"indexed\":true},{\"type\":\"address\",\"name\":\"to\",\"indexed\":true},{\"type\":\"uint256\",\"name\":\"value\",\"indexed\":false}]},{\"type\":\"event\",\"name\":\"Approval\",\"inputs\":[{\"type\":\"address\",\"name\":\"owner\",\"indexed\":true},{\"type\":\"address\",\"name\":\"spender\",\"indexed\":true},{\"type\":\"uint256\",\"name\":\"value\",\"indexed\":false}]},{\"type\":\"event\",\"name\":\"Transfer\",\"inputs\":[{\"type\":\"address\",\"name\":\"from\",\"indexed\":true},{\"type\":\"address\",\"name\":\"to\",\"indexed\":true},{\"type\":\"uint256\",\"name\":\"tokenId\",\"indexed\":true}]}]"}' \ - --name erc20-events -``` - -### Inspect with revision history - -Every `add` and `remove` creates a new revision. To see the full history: - -```shell -viewkit view inspect erc20-events --verbose -``` - -This shows the current state (query, SDL, lenses) and all past revisions, each with a version number. - -### Roll back if something went wrong - -If the updated ABI doesn't work as expected, revert to the previous version: - -```shell -# roll back to the most recent previous version -viewkit view rollback erc20-events -``` - -Or roll back to a specific version: - -```shell -viewkit view rollback erc20-events --version 3 -``` - -### Test and redeploy - -```shell -# validate the updated view compiles -viewkit view test erc20-events - -# deploy locally to verify in the playground -viewkit view deploy erc20-events --target local - -# once verified, deploy to devnet -viewkit view deploy erc20-events --target devnet --rpc http://34.29.171.79:8545/ -``` - -### Delete a view (if needed) - -To remove a view bundle from your local machine entirely: - -```shell -viewkit view delete erc20-events -``` - -This deletes the local bundle. It does not remove a view that has already been deployed to devnet. On-chain registrations are permanent. To update a deployed view, deploy a new version with the same name. - -## Querying a deployed view - -GraphQL queries you can run against a deployed View's output collection. These examples assume the `erc20-events` View from Example 3 is deployed and receiving data. - -### Basic query for the latest 10 events - -```graphql -{ - EventView( - order: { blockNumber: DESC } - limit: 10 - ) { - hash - from - to - blockNumber - logAddress - event - signature - arguments - } -} -``` - -### Filter by contract and event type - -```graphql -{ - EventView( - filter: { - _and: [ - { logAddress: { _eq: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } } - { event: { _eq: "Transfer" } } - ] - } - limit: 10 - ) { - hash - from - to - arguments - blockNumber - } -} -``` - -### Filter by block range - -```graphql -{ - EventView( - filter: { blockNumber: { _gte: 19540000 } } - ) { - hash - from - to - event - blockNumber - } -} -``` - -### Filter by transaction hash - -```graphql -{ - EventView( - filter: { hash: { _eq: "0xabc123..." } } - ) { - hash - from - to - event - signature - arguments - blockNumber - } -} -``` - -For the full list of Viewkit commands and GraphQL filter operators, see the [Viewkit reference](/reference/components/viewkit/). For more on lenses, available modules, and how to chain them, see the [Lenses guide](/reference/components/lens/). For troubleshooting and common errors, see the [FAQ](/run/operations/troubleshooting/). - -## Need help - -{{ need_help(client="Viewkit", repo_name="shinzo-view-creator", repo="https://github.com/shinzonetwork/shinzo-view-creator/issues") }} diff --git a/content/build/how-to/configure-attestation-thresholds/index.md b/content/build/how-to/configure-attestation-thresholds/index.md index 5f722c8..53a9a46 100644 --- a/content/build/how-to/configure-attestation-thresholds/index.md +++ b/content/build/how-to/configure-attestation-thresholds/index.md @@ -3,4 +3,97 @@ title = "Configure attestation thresholds" description = "How to require a minimum number of Generator attestations before query results are returned in your app." +++ -This page is coming soon. +Shinzo data is signed by the Generator clients that produced it, and Host clients keep attestation records that track how many independent Generator clients signed the same data. With the app-sdk you can set a bar: only return documents whose attestation count meets your threshold. The threshold is a query-time filter, not a system-wide setting, so one app can apply different bars to different queries. + +{% admonition(type="warning") %} +The attestation helpers described here live on the `Feature/attestationFilter` branch of the app-sdk and are not merged into `main` yet. They also depend on pushed replication, which is currently blocked by the DefraDB version mismatch described in [Subscribe to Views with the app-sdk](/build/how-to/subscribe-to-views/). This page documents the API as implemented on that branch so you can build against it ahead of the merge. +{% end %} + +## Add attestation records for a View + +Attestation records are segmented per View, so your app only receives records for the data it cares about. Opt in per View with `AddAttestationRecordCollection`: + +```go +import "github.com/shinzonetwork/shinzo-app-sdk/pkg/attestation" + +err := attestation.AddAttestationRecordCollection(context.Background(), myNode, myView.Name) +if err != nil { + if strings.Contains(err.Error(), "collection already exists") { + // Records for this View were added before. Informational and safe to ignore. + } else { + panic(err) + } +} +``` + +This works like `SubscribeTo`: it adds an `AttestationRecord_` collection to your embedded DefraDB instance and registers it for passive replication, so Host clients push the View's attestation records alongside its documents. Call it once per View you want to filter, after subscribing to the View itself. + +## Choose configured or per-query thresholds + +Four helpers cover the two ways to set the bar. All four work like `defra.QuerySingle` and `defra.QueryArray`, except they drop results that fail the attestation check. Your result struct needs a `DocID` field, because the filter matches documents to their attestation records by DocID. + +The configured pair reads the threshold from `shinzo.minimum_attestations` in your config: + +```go +transfers, err := attestation.QueryArrayWithConfiguredAttestationFilter[Transfer](ctx, myNode, query) +transfer, err := attestation.QuerySingleWithConfiguredAttestationFilter[Transfer](ctx, myNode, query) +``` + +The per-call pair takes the threshold as an argument: + +```go +transfers, err := attestation.QueryArrayWithAttestationFilter[Transfer](ctx, myNode, query, 3) +transfer, err := attestation.QuerySingleWithAttestationFilter[Transfer](ctx, myNode, query, 3) +``` + +Set the config default in `config.yaml`: + +```yaml +shinzo: + minimum_attestations: 2 +``` + +Which style to use depends on how uniform your trust requirements are: + +| Situation | Approach | +| --- | --- | +| One threshold covers the whole app | Configured helpers with `minimum_attestations` | +| A wallet display where showing something fast beats certainty | Per-call threshold of 1 | +| A high-value flow like a settlement or payout | Per-call threshold of 3 or more | +| A mix of casual and critical reads in one app | Configured default, per-call overrides where it matters | + +Make sure you added the attestation record collection for any View you query through these helpers. Without it there are no records to filter on, and every result fails the check. + +## Debug an empty result set + +If a filtered query returns nothing but the unfiltered equivalent has rows, the filter is doing its job and your data is under-attested. Inspect the records directly to see why. An attestation record ties one of your View's documents to the evidence behind it: + +- `attested_doc` is the DocID of the View document being attested to. +- `source_doc` links back to the source document the attestation came from. +- `CIDs` are the signed commit CIDs backing the attestation. +- `doc_type` names the attested collection. +- `vote_count` is a CRDT counter that Host clients increment as they observe more Generator clients signing the same data. + +Query the records for the document that went missing: + +```graphql +{ + ____AttestationRecord( + filter: { attested_doc: { _eq: "" } } + ) { + attested_doc + source_doc + CIDs + doc_type + vote_count + } +} +``` + +If `vote_count` (or the number of records) is below your threshold, the filter correctly excluded the document. Lower the threshold, or wait for more Generator clients to attest. Note that attestations only accumulate while Generator clients are actually signing the underlying data, so a quiet View on a testnet may legitimately sit at a low count. + +To check signatures and CIDs by hand, see [Verify data with signatures and CIDs](/build/how-to/verify-data/). For the reasoning behind per-query trust, see [Attestation as a query filter](/build/explanation/attestation-as-a-query-filter/), and [Attestation](/understand/core-concepts/attestation/) for the platform-level picture. + +## Need help + +{{ need_help(client="app-sdk", repo_name="app-sdk", repo="https://github.com/shinzonetwork/app-sdk/issues") }} diff --git a/content/build/how-to/connect-to-a-host/index.md b/content/build/how-to/connect-to-a-host/index.md index 18b9035..226a1f1 100644 --- a/content/build/how-to/connect-to-a-host/index.md +++ b/content/build/how-to/connect-to-a-host/index.md @@ -3,4 +3,87 @@ title = "Connect your app to a Host" description = "How to connect an application to a Shinzo Host: P2P connection strings for embedded Go apps and GraphQL endpoints for direct-query TypeScript apps." +++ -This page is coming soon. +How you connect to a Host depends on your architecture. A local-first Go app embeds DefraDB and peers with Hosts over libp2p, so it needs connection strings. A direct-query TypeScript app talks to a Host's GraphQL endpoint over HTTP, so it needs an endpoint URL. Both start with the discovery step in [Find Views and Hosts](/build/how-to/find-views-and-hosts/), which explains where each value comes from. + +A Host client exposes four interfaces: + +| Port | Interface | +| --- | --- | +| 9181 | GraphQL API at `/api/v0/graphql` | +| 9182 | GraphQL playground, when enabled | +| 9171 | libp2p peering | +| 8080 | Health and self-description endpoints | + +Public Hosts vary in how they publish these, so always read the actual `connection_string` and `endpoint_address` from the registry rather than assuming ports. + +## Check a Host's health first + +Before wiring a Host into your app, check that it is alive and processing data: + +```shell +curl -s -H "Accept: application/json" http://:8080/health | jq '{status, current_block}' +``` + +```json +{ + "status": "healthy", + "current_block": 25903651 +} +``` + +The `Accept: application/json` header asks for the JSON form; a browser gets an HTML status page instead. A `healthy` status with a recent `current_block` means the Host is peered and syncing. The same server answers `GET /registration` with the Host's DID, `connection_string`, and `endpoint_address`, which is handy for confirming a Host's identity before you trust its data. + +## Embedded app (Go) + +A local-first app receives pushed data over libp2p, so the connection happens in the app-sdk config. Take the Host's `connection_string` from the registry: + +```shell +curl -s http://testnet.shinzo.network:1317/shinzonetwork/host/v1/hosts \ + | jq -r '.hosts[].connection_string' +``` + +Add one or more of them to `defradb.p2p.bootstrap_peers` in your `config.yaml`: + +```yaml +defradb: + p2p: + enabled: true + bootstrap_peers: + - "/ip4/34.66.172.230/tcp/9171/p2p/12D3KooWKVCMswzcXYe9kW2z7nSB9YUWjVVLsMbJnBjVPFUQkbQ7" + listen_addr: "/ip4/127.0.0.1/tcp/9171" +``` + +The full multiaddr form `/ip4//tcp/9171/p2p/` always works, but it is not required. Bare IPs (`34.66.172.230`) and `ip:port` pairs (`34.66.172.230:9171`) also work, because the client discovers the peer ID during the connection handshake and fills it in for you. Listing several bootstrap peers makes the first connection more reliable, since registered Hosts come and go on a testnet. + +Once peered, subscribe to a View and data starts arriving. [Subscribe to Views with the app-sdk](/build/how-to/subscribe-to-views/) covers that flow. + +## Direct-query app (TypeScript) + +A direct-query app never peers with anything. It reads the Host's `endpoint_address` from the registry and POSTs signed GraphQL requests to it: + +```shell +curl -s http://testnet.shinzo.network:1317/shinzonetwork/host/v1/hosts \ + | jq -r '.hosts[].endpoint_address' +``` + +The endpoint already includes the API path, so you POST straight to it: + +```shell +curl -s -X POST "http://34.66.172.230/api/v0/graphql" \ + -H "Content-Type: application/json" \ + -d '{"query": "{ Erc20Event(limit: 1) { blockNumber } }", "extensions": { ... } }' +``` + +View queries carry a signature in the `extensions` envelope, and one billed query maps to one pool: a request may touch only one View collection, and its `pool_address` extension names the pool it bills to. When a Host enforces billing, rejections come back as plain errors: `403` if the request signature is missing, stale, or fails verification, and `402` if the signer's query balance is too low. [Query your first View](/build/tutorials/query-your-first-view/) builds the signing flow end to end. + +{% admonition(type="note") %} +Billing enforcement is rolling out on the testnet, so some Hosts still answer unsigned queries. Signed requests are the supported interface either way; treat unsigned access as a convenience that will go away. +{% end %} + +## Prefer your own Host + +Public Hosts are shared infrastructure. If you want guaranteed availability, or you do not want a third party to see your queries at all, you can point everything above at a Host you run yourself. See [Use your own infrastructure](/build/how-to/use-your-own-infrastructure/). + +## Need help + +{{ need_help(client="Host", repo_name="shinzo-host-client", repo="https://github.com/shinzonetwork/shinzo-host-client/issues") }} diff --git a/content/build/how-to/find-views-and-hosts/index.md b/content/build/how-to/find-views-and-hosts/index.md index 627bf35..4d3d91b 100644 --- a/content/build/how-to/find-views-and-hosts/index.md +++ b/content/build/how-to/find-views-and-hosts/index.md @@ -3,4 +3,122 @@ title = "Find Views and Hosts" description = "How to discover registered Views, serving Hosts, and pools on the Shinzo network using the Explorer, Studio, the REST API, or the TypeScript SDK." +++ -This page is coming soon. +Views, Hosts, and Generators are all registered on ShinzoHub, so discovery is a matter of reading the registry. There are four ways to do it: the Explorer in a browser, Shinzo Studio, the chain's REST API, or the TypeScript SDK. Pick whichever fits the job; they all read the same on-chain state. + +Two fields come up constantly, so get familiar with them first: + +- `connection_string` is a libp2p multiaddr, like `/ip4/34.66.172.230/tcp/9171/p2p/12D3KooW...`. It is how peers dial each other. Your local-first app needs it. +- `endpoint_address` is the URL of a Host's GraphQL API, like `http://34.66.172.230/api/v0/graphql`. It is what direct-query apps POST to. + +## Browse the Explorer + +The [Shinzo Explorer](https://explorer.shinzo.network/shinzohub) renders the registry as web pages. Open it and switch between the **Blocks**, **Transactions**, **Generators**, **Hosts**, and **Validators** tabs. + +The **Hosts** tab lists every registered Host client with its connection details, and the **Generators** tab lists the registered Generator clients. Use the Explorer when you want to eyeball the network: who is online, what is registered, and whether anything changed recently. + +## Browse the View catalog in Studio + +[Shinzo Studio](https://studio.shinzo.network/) shows registered Views in its catalog, each with its definition, pool, and serving Hosts. Studio is the better browser experience when your question is "what Views exist and what do they return", because it shows the View's SDL alongside its network status. You can also deploy your own View from the same UI; see [Create and deploy Views in Shinzo Studio](/build/how-to/use-shinzo-studio/). + +## Query the REST API + +ShinzoHub exposes a REST API at `http://testnet.shinzo.network:1317`. This is the option for scripts and terminals, and the responses below are real testnet output. + +List registered Views: + +```shell +curl -s "http://testnet.shinzo.network:1317/shinzonetwork/view/v1/views?include_data=true" \ + | jq -r '.views[] | .name + " " + .address' +``` + +```output +Studio_v1_Erc20TransferUSDC 0xD3084cAddCe8E1bab07C6eDd8afb835566904C6B +Erc20Event 0xEAc245f905e0aAcF3b9Fe27153F2AaF485dc1B48 +FilteredAndDecodedLogs 0xa1226B03c54789e9Bf8876ac956aBbD1bDf5B654 +... +``` + +List registered Host clients: + +```shell +curl -s "http://testnet.shinzo.network:1317/shinzonetwork/host/v1/hosts" \ + | jq -r '.hosts[] | .connection_string + " " + .endpoint_address' +``` + +```output +/ip4/34.66.172.230/tcp/9171/p2p/12D3KooWKVCMswzcXYe9kW2z7nSB9YUWjVVLsMbJnBjVPFUQkbQ7 http://34.66.172.230/api/v0/graphql +/ip4/34.63.186.249/tcp/9171/p2p/12D3KooWSqvLctTtcQLvqSVZU4sTCUWxCX9z4NeFpSHnmVWBiFMZ http://34.63.186.249/api/v0/graphql +... +``` + +List registered Generator clients: + +```shell +curl -s "http://testnet.shinzo.network:1317/shinzonetwork/indexer/v1/indexers" \ + | jq -r '.indexers[] | .operator_address + " " + .source_chain' +``` + +List the pools serving a View, with membership and activity: + +```shell +curl -s "http://testnet.shinzo.network:1317/shinzonetwork/pool/v1/views/0xEAc245f905e0aAcF3b9Fe27153F2AaF485dc1B48/pools" \ + | jq -c '.details[] | {pool: .pool.pool_address, hosts: (.hosts | length), is_active}' +``` + +```output +{"pool":"0xDbc3bE7CBd8Dc8901E3BbbeA1A740BE490dAe23B","hosts":3,"is_active":true} +``` + +A pool becomes active once at least 3 Hosts have joined it, so `is_active` is the quick check for whether a View is being served. The registry moves over time, so expect different names and addresses when you run these. + +## Query with the TypeScript SDK + +The same registry reads are available from `@shinzo/shinzohub` if your app needs them programmatically. The client extends a viem public client with ShinzoHub actions: + +```ts +import { createPublicClient, http } from "viem"; +import { shinzoHubActions } from "@shinzo/shinzohub"; +import { shinzoHubTestnet } from "@shinzo/shinzohub/chains"; + +const client = createPublicClient({ + chain: shinzoHubTestnet, + transport: http(), +}).extend(shinzoHubActions); + +const { views } = await client.listViews({ limit: 25, includeMetadata: true }); +const { hosts } = await client.listHosts({ limit: 100 }); +const pools = await client.listViewPools({ viewAddress: views[0].viewAddress }); +``` + +`listViews` returns registered Views (with the parsed query, SDL, and lens metadata when `includeMetadata` is set), `listHosts` returns Host clients with their `endpointAddress`, and `listViewPools` returns the pools for a View with member Hosts and an `isActive` flag. `getNetworkUnitPrice` reads the network-wide unit price for queries. The [Query your first View](/build/tutorials/query-your-first-view/) tutorial walks through combining these into a full query flow. + +## Ask a Host about itself + +Any running Host client describes itself over its health server, on port 8080 by default: + +```shell +curl -s -H "Accept: application/json" http://:8080/registration | jq .registration +``` + +```json +{ + "did": "did:key:zQ3shQKyThhTw3M83ZcTobUQER5mxNbSrW3Yrds3if4mPPrje", + "connection_string": "/ip4/203.0.113.10/tcp/9171/p2p/12D3KooWB1K1k67DNEcxShBq3o15LQrKHcBkRxzv6AuFR8p8idqJ", + "endpoint_address": "http://203.0.113.10/api/v0/graphql" +} +``` + +This is the same information a Host publishes to the registry when it registers, so it is the quickest way to check what a specific Host claims to be. See [Connect your app to a Host](/build/how-to/connect-to-a-host/) for the health endpoint and the rest of the port layout. + +## What does not exist + +Viewkit has no list or discovery commands. It builds and deploys View bundles, and that is all. Discovery happens on-chain through the paths above, or through the Explorer and Studio UIs listed in [Tools](/reference/tools/). + +## Where to next + +- [Connect your app to a Host](/build/how-to/connect-to-a-host/) to put a `connection_string` or `endpoint_address` to work. +- [Query your first View](/build/tutorials/query-your-first-view/) for the full flow from discovery to a signed query. + +## Need help + +{{ need_help(client="Host", repo_name="shinzo-host-client", repo="https://github.com/shinzonetwork/shinzo-host-client/issues") }} diff --git a/content/build/how-to/query-data/index.md b/content/build/how-to/query-data/index.md index 9461817..c627e20 100644 --- a/content/build/how-to/query-data/index.md +++ b/content/build/how-to/query-data/index.md @@ -1,6 +1,217 @@ +++ title = "Query data" +aliases = ["/hosts/examples", "/build/query-data/"] description = "How to query Shinzo data with GraphQL: filters, ordering, nested documents, DocIDs, CIDs, and signatures." +++ -This page is coming soon. +You query Shinzo data with GraphQL, and there are two places to run it. A local-first app queries its embedded DefraDB instance through the app-sdk helpers, with no network call. A direct-query app POSTs the same query to a Host client's `/api/v0/graphql` endpoint over HTTP. Same language, same collections, same filter operators on both. Only the transport changes. [Connect your app to a Host](/build/how-to/connect-to-a-host/) covers the wiring. + +Here are the patterns you'll reach for most. The examples use primitive collections (blocks, transactions, logs), and they all work against a View's output collection too. + +{% admonition(type="note") %} +Collection names are prefixed with `____`, derived from the `chain.name` and `chain.network` settings of the Generator client that indexed the data (for example `____Block` or `Optimism__Mainnet__Block`). The examples below use the `____` placeholder. Substitute the prefix that matches your chain. See the [chain config](/run/run-a-generator/config-reference#chain) for details. +{% end %} + +## Get the latest N documents + +Order by a field and cap the result with `limit`. This is the pattern behind most "recent activity" displays. + +```graphql +{ + ____Block(limit: 10, order: { number: DESC }) { + _docID + number + timestamp + hash + } +} +``` + +The same shape works for a View. This query fetches the 10 most recent decoded events from a View collection: + +```graphql +{ + EventView(limit: 10, order: { blockNumber: DESC }) { + hash + from + to + blockNumber + logAddress + event + arguments + } +} +``` + +## Fetch a document by DocID or CID + +When you already know a document's `_docID`, pass it as the `docID` argument to fetch exactly that document: + +```graphql +{ + ____Transaction(docID: "bae-25fb059c-f232-5305-8a5d-0162f01e43e6") { + _docID + blockHash + blockNumber + hash + to + from + value + } +} +``` + +Documents are also content-addressed. Passing a commit CID as the `cid` argument resolves the document at that exact version: + +```graphql +{ + ____Transaction(cid: "bafyreibtbym4uht5dppohohg4wg66tdg4r253ws2i4wshc2gtwje6e25sy") { + _docID + blockHash + blockNumber + hash + to + from + value + } +} +``` + +```json +{ + "data": { + "____Transaction": [ + { + "_docID": "bae-25fb059c-f232-5305-8a5d-0162f01e43e6", + "blockHash": "0x9ea35b3bd9e71c57617cc30394b22f607b735f2eea7a0db974cf02ad54de98fb", + "blockNumber": 23902272, + "from": "0x654a6BCe2C6F0aF68eAdCFEaD06bB49C398B3F98", + "hash": "0x61b79fc417ef183e1798681c59481410dd79f919d11806a6e7e77ebd0a744f78", + "to": "0x677f857da5e7C42b823655290cc40ff401e138D3", + "value": "1000000000" + } + ] + } +} +``` + +You usually get a CID from a document's `_version` field or from an attestation record. [Verify data with signatures and CIDs](/build/how-to/verify-data/) covers that flow. + +## Filter by field values + +The `filter` argument narrows results with operators like `_eq`, `_geq`, `_and`, and `_like`. This query returns blocks above a height: + +```graphql +{ + ____Block(filter: { number: { _geq: 19540000 } }) { + _docID + number + hash + } +} +``` + +Combine conditions with `_and`. This query returns `Transfer` events decoded from one contract: + +```graphql +{ + EventView( + filter: { + _and: [ + { logAddress: { _eq: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } } + { event: { _eq: "Transfer" } } + ] + } + limit: 10 + ) { + hash + from + to + arguments + blockNumber + } +} +``` + +The full operator table lives in the [Viewkit reference](/reference/components/viewkit/#filter-operators). + +## Get a block with nested data + +Relations are nested in the query, so one round trip fetches a block with its transactions and their logs: + +```graphql +{ + ____Block(limit: 1) { + _docID + number + timestamp + hash + gasUsed + gasLimit + baseFeePerGas + parentHash + miner + transactions { + hash + transactionIndex + _docID + logs { + transactionHash + address + topics + data + } + } + } +} +``` + +Nested selections accept their own `filter`, `order`, and `limit` arguments, so you can shape each level independently. + +## Count the transactions in a block + +There is no aggregate count field, but the `transactionIndex` values within a block are zero-based and contiguous. Fetch the highest `transactionIndex` and add one: + +```graphql +{ + ____Block(filter: { number: { _eq: 23901130 } }) { + number + transactions( + limit: 1 + filter: { blockNumber: { _eq: 23901130 } } + order: { transactionIndex: DESC } + ) { + transactionIndex + } + } +} +``` + +The total transaction count is the returned `transactionIndex` plus 1. + +## Check who signed a document + +Every document carries signed commits. Selecting `_version` returns the CID and signature for each commit, which is the starting point for verifying data: + +```graphql +{ + ____Block(limit: 10, order: { number: DESC }) { + number + _docID + _version { + cid + signature { + identity + value + type + } + } + } +} +``` + +The `identity` field is the public key of the Generator client that signed the commit. For commit metadata, attestation records, and CID navigation, see [Verify data with signatures and CIDs](/build/how-to/verify-data/). + +## Need help + +{{ need_help(client="Host", repo_name="shinzo-host-client", repo="https://github.com/shinzonetwork/shinzo-host-client/issues") }} diff --git a/content/build/how-to/subscribe-to-views/index.md b/content/build/how-to/subscribe-to-views/index.md index 9898a5e..94318d9 100644 --- a/content/build/how-to/subscribe-to-views/index.md +++ b/content/build/how-to/subscribe-to-views/index.md @@ -3,4 +3,157 @@ title = "Subscribe to Views with the app-sdk" description = "How to configure and start an embedded DefraDB instance, subscribe to Views, and receive pushed data in a Go application." +++ -This page is coming soon. +A local-first app embeds a DefraDB instance through the [app-sdk](https://github.com/shinzonetwork/shinzo-app-sdk), subscribes to Views, and lets Host clients push pre-processed data to it over P2P. This page covers the mechanics: configuration, startup and shutdown, subscribing, and querying what arrives. For why the model works this way, see [The Shinzo app model](/build/explanation/the-app-model/). For filtering results by Generator attestations, see [Configure attestation thresholds](/build/how-to/configure-attestation-thresholds/). + +Install the SDK with Go modules: + +```shell +go get github.com/shinzonetwork/shinzo-app-sdk +``` + +{% admonition(type="warning") %} +Pushed replication is currently blocked by a version mismatch. The app-sdk pins DefraDB v0.20 while public Host clients run DefraDB v1.0, and documents published by a v1.0 Host cannot be parsed by a v0.20 app, so nothing arrives yet. Everything on this page is correct against the current app-sdk: your app connects and subscribes successfully, and its queries return empty until the SDK ships a compatible DefraDB. If you need data in a Go app today, query a Host directly instead, as described in [Query data](/build/how-to/query-data/). +{% end %} + +## Configure the SDK + +The SDK loads a YAML config file. A minimal one for an app that only holds Shinzo data: + +```yaml +defradb: + url: "http://localhost:9181" + keyring_secret: "dev-secret" + p2p: + enabled: true + bootstrap_peers: + - "/ip4/34.66.172.230/tcp/9171/p2p/12D3KooWKVCMswzcXYe9kW2z7nSB9YUWjVVLsMbJnBjVPFUQkbQ7" + listen_addr: "/ip4/127.0.0.1/tcp/9171" + store: + path: "./.defra" + +shinzo: + minimum_attestations: 1 + +logger: + development: true +``` + +Three keys deserve attention: + +- `defradb.keyring_secret` encrypts the local keyring that holds your node's identity, so the app keeps the same P2P identity across restarts. It can also come from the `DEFRA_KEYRING_SECRET` environment variable. +- `defradb.p2p.enabled` must be `true`, or the instance starts with networking off and nothing can be pushed to it. `defradb.p2p.bootstrap_peers` lists the Host clients to dial; [Connect your app to a Host](/build/how-to/connect-to-a-host/) shows where to get theirs. +- `shinzo.minimum_attestations` sets the default attestation threshold for filtered queries. It only needs a valid value until you start using the attestation query helpers. +- `logger.development` keeps DefraDB's logs visible while you develop. Set it to `false` in production; DefraDB logs a lot. + +Load the file with `config.LoadConfig`: + +```go +shinzoConfig, err := config.LoadConfig("config.yaml") +if err != nil { + panic(err) +} +``` + +In tests, where the working directory is unpredictable, the `file.FindFile` helper locates the config by walking up from the current directory: + +```go +configPath, err := file.FindFile("config.yaml") +if err != nil { + panic(err) +} +shinzoConfig, err := config.LoadConfig(configPath) +``` + +If you pass `nil` instead of a loaded config, the SDK builds a default config for you. That is fine for a quick experiment, but real apps should manage a file: the defaults cannot know your bootstrap peers. + +## Start and stop the embedded instance + +`defra.StartDefraInstance` boots the embedded DefraDB node. Its second argument is a `SchemaApplier`, which decides what non-View schema gets applied at startup: + +```go +myNode, _, err := defra.StartDefraInstance( + shinzoConfig, + &defra.MockSchemaApplierThatSucceeds{}, + nil, + nil, +) +if err != nil { + panic(err) +} +defer myNode.Close(context.Background()) +``` + +The two `nil` arguments are optional node options and a replication filter, which most apps do not need. The second return value is the network handler, which you can ignore for basic subscriptions. + +Pick the `SchemaApplier` that matches how you use DefraDB: + +- `MockSchemaApplierThatSucceeds` applies nothing. Use it when DefraDB only holds Shinzo data, which is the common case. +- `SchemaApplierFromFile` reads a schema from a file, and `SchemaApplierFromProvidedSchema` takes a schema string. Use either when your app also stores its own documents in the same DefraDB instance; your collections go in the schema, and View collections arrive later through subscriptions. + +Whatever you start, close it. `myNode.Close(context.Background())` shuts the node down cleanly, and `defer` is the easiest way to guarantee it runs. + +## Subscribe to a View + +Subscribing is what turns a registered View into pushed data. Define the View and call `SubscribeTo`: + +```go +sdl := `type Studio_v1_Erc20TransferUSDC { + tokenAddress: String + hash: String + blockNumber: Int + from: String + to: String + amount: String +}` + +view := views.View{ + Name: "Studio_v1_Erc20TransferUSDC", + Sdl: &sdl, +} + +err = view.SubscribeTo(context.Background(), myNode) +if err != nil { + if strings.Contains(err.Error(), "collection already exists") { + // You have subscribed before. The error is informational and safe to ignore. + } else { + panic(err) + } +} +``` + +Only the `Name`, `Sdl`, and optionally `Query` fields of the `View` struct matter for subscribing. The `Query` field is not shown here because the SDL alone is enough to receive and store documents. + +`SubscribeTo` does two things. It applies the View's SDL to your embedded instance, so the collection exists locally and can be queried. And it registers that collection as a topic in DefraDB's passive replication, which is the signal that tells connected Host clients to push the View's documents to you. + +The "collection already exists" error is common and expected: it means you have subscribed to this View before, so the collection is already in place. It is informational and safe to ignore. Other errors are real and should not be swallowed. + +## Query the pushed data + +Once subscribed, Host clients push the View's documents into your local collection as they process new blocks. Query the collection with the SDK's generic helpers and a Go struct that matches the fields: + +```go +type Transfer struct { + TokenAddress string `json:"tokenAddress"` + From string `json:"from"` + To string `json:"to"` + Amount string `json:"amount"` + BlockNumber int `json:"blockNumber"` +} + +transfers, err := defra.QueryArray[Transfer]( + context.Background(), + myNode, + `query { Studio_v1_Erc20TransferUSDC(limit: 10) { tokenAddress from to amount blockNumber } }`, +) +if err != nil { + panic(err) +} +``` + +`defra.QueryArray[T]` returns a slice of `T`, and `defra.QuerySingle[T]` returns one document when you expect exactly one. The GraphQL itself is the same language you would run against a Host's endpoint, so [Query data](/build/how-to/query-data/) applies here too. + +Until the DefraDB version gap described at the top is closed, these queries return empty results. Once a compatible app-sdk release lands, the same code starts returning rows a few moments after subscribing. + +## Need help + +{{ need_help(client="app-sdk", repo_name="app-sdk", repo="https://github.com/shinzonetwork/app-sdk/issues") }} diff --git a/content/build/how-to/use-shinzo-studio/index.md b/content/build/how-to/use-shinzo-studio/index.md index 02a69c0..f3ed0bd 100644 --- a/content/build/how-to/use-shinzo-studio/index.md +++ b/content/build/how-to/use-shinzo-studio/index.md @@ -3,4 +3,49 @@ title = "Create and deploy Views in Shinzo Studio" description = "How to create, deploy, and query Views from your browser with Shinzo Studio, without installing the CLI." +++ -This page is coming soon. +[Shinzo Studio](https://studio.shinzo.network/) covers the same create, deploy, and query flow as Viewkit for developers who prefer a browser UI. Nothing installs locally; you need a browser wallet and some testnet SHNZ. + +## Connect your wallet + +1. Get testnet SHNZ from the [faucet](https://faucet.shinzo.network/) if you do not have any. Deploying a View and funding a query balance are on-chain transactions, so the wallet needs a small balance for network fees on top of whatever you spend. +1. Open [studio.shinzo.network](https://studio.shinzo.network/) and click **Connect Wallet**. +1. Choose a wallet from the list and approve the connection. +1. Studio asks you to switch to the Shinzo network. Confirm the network change in your wallet when prompted. The network uses chain ID 91273001; depending on the Studio deployment, your wallet may label it Shinzo testnet or Shinzo devnet. + +Once connected, Studio shows your wallet balance and query balance, and the View catalog becomes available. + +## Create a View + +1. Start a new View from the catalog. Studio offers templates for the common cases, built on the same prebuilt lenses the CLI uses: **Decode Contract Events** (decodes verified event logs for one contract on Ethereum mainnet), **ERC20 Transfers** (normalized transfer rows for one token contract), and **ERC20 Balances** (account balances and transfer counts for one token contract). +1. Enter what the template asks for. For the decode template that is the contract address, for example `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48` for USDC. Studio fetches the contract's verified ABI from Sourcify and picks out the event definitions, so the ABI has to be verified there. If Sourcify has no verified ABI for the contract, Studio tells you and stops. +1. Review the generated View definition: the source query, the SDL, and the lens definition with its arguments. This is exactly the bundle Viewkit would produce for the same inputs, and it is worth reading before you sign anything. + +## Deploy the View + +1. Click **Deploy View**. Studio validates the definition, builds the View bundle, and asks your wallet to sign the registration transaction. +1. Confirm in your wallet. Studio sends the transaction and waits for confirmation. +1. Wait for registration to complete. Registration on ShinzoHub finishes asynchronously, so Studio polls until the View reaches REGISTERED status. This can take a short while; you can inspect the View definition in the meantime. + +After registration, Studio prompts you to back the View with demand. Creating demand bonds SHNZ to the View's pool and is how Host clients see that the View should be served. The first demand creates the pool, and the pool becomes active once at least 3 Hosts have joined it. Until then Studio shows the pool as waiting for Hosts. + +## Fund your query balance + +1. Open your View and click **Add query funds** (also shown as **Add to query balance**). +1. Enter an amount of SHNZ and approve the deposit transaction in your wallet. +1. Wait for the **Query balance funded** confirmation. + +{% admonition(type="warning") %} +Query billing is still being finalized on the testnet, and the details here may change. Fund small amounts for now, and expect the metering rules to evolve. There is no dedicated billing guide yet. +{% end %} + +## Query the View + +1. Open the View and click **Query this view**. +1. Pick a Host with **Select direct query host**. Studio lists the Hosts currently serving the View's pool, so choose one of those; a Host outside the pool does not have the View's data. +1. Write or adjust the GraphQL query in the editor and run it. Each run asks your wallet for one signature and spends from your prepaid query balance, so keep an eye on the balance Studio shows. If it runs out, Studio refuses to send and tells you to add funds first. + +The results come straight from the Host you picked. To script the same flow outside the browser, see [Query your first View](/build/tutorials/query-your-first-view/). To build the same View from the terminal instead, see [Create your first View](/build/tutorials/create-your-first-view/). + +## Need help + +{{ need_help(client="Shinzo SDK", repo_name="web", repo="https://github.com/shinzonetwork/web/issues") }} diff --git a/content/build/how-to/use-your-own-infrastructure/index.md b/content/build/how-to/use-your-own-infrastructure/index.md index b54b88f..d8db76f 100644 --- a/content/build/how-to/use-your-own-infrastructure/index.md +++ b/content/build/how-to/use-your-own-infrastructure/index.md @@ -3,4 +3,33 @@ title = "Use your own infrastructure" description = "How to run your own Host as a private Direct Client for your app: the strongest privacy and control, at the cost of operating a node." +++ -This page is coming soon. +Everything in the Build apps section works against public infrastructure, and for most apps that is the right trade. But public Hosts are shared: they see your queries, their availability is not yours to control, and your app inherits their trust posture. Running your own Host client as a private Direct Client flips that. You get the strongest query privacy available today and full control over the data path, at the cost of operating a node. + +The privacy point is worth stating plainly. When your app queries a public Host, that Host's operator can observe what you ask. When your app embeds DefraDB and receives pushed data, the Hosts you peer with still see what you subscribe to. Querying a Host you run yourself is the only configuration today where no third party can observe your queries, because there is no third party in the path at all. [Privacy](/understand/core-concepts/privacy/) lays out the full model. + +## Pick a privacy tier + +A private Host client comes in two tiers, and the difference is how much of the public network it talks to. + +A standard private setup keeps ShinzoHub connected so your Host still fetches and runs the public Views, but makes the Host invisible: you skip registration, and you set `defradb.p2p.bootstrap_peers` to your own Generator client only. You get the public Views without exposing your read path to the network. + +A fully air-gapped setup cuts ShinzoHub off entirely by setting `shinzo.hub_base_url` to an empty string. The Host contacts nothing except the Generator client you point it at, and nothing about it is published anywhere. This is the most private setup possible, and it means you manage View definitions yourself. + +## Set it up + +This page is the bridge, not the guide. The run section has the real instructions: + +1. Run a Generator client to produce the signed primitive data. Start with [Run a Generator](/run/run-a-generator/install/) for installation and configuration. +1. Run a Host client against it. [Install a Host](/run/run-a-host/install/) covers the binary and ports, and [Private Hosts](/run/run-a-host/private-hosts/) walks through both privacy tiers with complete configs, including how to load Views onto an air-gapped Host. +1. Point your app at your Host exactly as you would at a public one. The `connection_string` and `endpoint_address` come from your Host's own `/registration` endpoint. + +Expect to operate real infrastructure: a synced source-chain node or a managed endpoint for the Generator, disk for DefraDB, and the usual monitoring. + +## Back to building + +- [Connect your app to a Host](/build/how-to/connect-to-a-host/): everything on that page works unchanged against your own Host. +- [Choosing an app architecture](/build/explanation/choosing-an-architecture/): where running your own Host sits relative to the other two models. + +## Need help + +{{ need_help(client="Host", repo_name="shinzo-host-client", repo="https://github.com/shinzonetwork/shinzo-host-client/issues") }} diff --git a/content/build/how-to/verify-data/index.md b/content/build/how-to/verify-data/index.md index 6023562..578fe32 100644 --- a/content/build/how-to/verify-data/index.md +++ b/content/build/how-to/verify-data/index.md @@ -3,4 +3,143 @@ title = "Verify data with signatures and CIDs" description = "How to verify who signed your data and navigate attestations, commits, and documents via CIDs." +++ -This page is coming soon. +Every document a Shinzo client serves is content-addressed and signed, so you can check where a piece of data came from instead of trusting the server that returned it. This page covers the verification queries: reading signatures off documents, tracing documents back to their attestations, and resolving CIDs to commits and documents. + +The examples query primitive collections through a Host client, and the same queries work in a local-first app's embedded DefraDB instance. Both surfaces share the query language, as described in [Query data](/build/how-to/query-data/). + +{% admonition(type="note") %} +Collection names are prefixed with `____`, derived from the `chain.name` and `chain.network` settings of the Generator client that indexed the data. Substitute the prefix that matches your chain. +{% end %} + +## Check who signed a document + +Every document carries signed commits in its `_version` field. Each entry has the commit's `cid` and a `signature` with the signer's `identity` (a public key), the signature `value`, and the signature `type`. + +```graphql +{ + ____Block(limit: 1, order: { number: DESC }) { + number + _docID + _version { + cid + signature { + identity + value + type + } + } + } +} +``` + +```json +{ + "data": { + "____Block": [ + { + "number": 23902272, + "_docID": "bae-91bd3f16-ccb1-5c35-b098-45672ee6fd48", + "_version": [ + { + "cid": "bafyreibtbym4uht5dppohohg4wg66tdg4r253ws2i4wshc2gtwje6e25sy", + "signature": { + "identity": "0348621aed3cb78ade074e86a3d650dfdfad0c110b274c0633b331d1b0a41ddd99", + "type": "ES256K", + "value": "MEUCIQCjfh3m0RNv4j094aW5YPEeF+GCMFWEGy0hiAcga7HKbQIgc54AV7WSdXZVyGH7jOuLcXJ6w5fDQSUdrlzgZhDkBTw=" + } + } + ] + } + ] + } +} +``` + +The `identity` is the public key of the Generator client that signed the commit. Comparing identities across documents tells you whether two pieces of data came from the same Generator client. + +## Trace a document back to its attestations + +Host clients maintain attestation records that track which Generator clients signed off on a document. Query the `AttestationRecord` collection and filter by the document you care about: + +```graphql +{ + ____AttestationRecord( + filter: { attested_doc: { _eq: "bae-91bd3f16-ccb1-5c35-b098-45672ee6fd48" } } + ) { + attested_doc + source_doc + CIDs + doc_type + vote_count + } +} +``` + +```json +{ + "data": { + "____AttestationRecord": [ + { + "attested_doc": "bae-91bd3f16-ccb1-5c35-b098-45672ee6fd48", + "source_doc": ["bae-25fb059c-f232-5305-8a5d-0162f01e43e6"], + "CIDs": ["bafyreibtbym4uht5dppohohg4wg66tdg4r253ws2i4wshc2gtwje6e25sy"], + "doc_type": "____Block", + "vote_count": 1 + } + ] + } +} +``` + +The fields matter for different reasons. `CIDs` links the record to the signed commits it attests to. `doc_type` names the attested collection. `vote_count` is a CRDT counter that goes up as more Generator clients are observed signing the same data, so it tells you how much independent agreement the document has. To filter query results by that count automatically, see [Configure attestation thresholds](/build/how-to/configure-attestation-thresholds/). + +## Resolve a CID to its commit or document + +A CID from `_version` or from an attestation record resolves in two directions. + +Query `_commits` for the commit-level metadata, including the signature over that exact commit: + +```graphql +{ + _commits(cid: "bafyreibtbym4uht5dppohohg4wg66tdg4r253ws2i4wshc2gtwje6e25sy") { + cid + docID + fieldName + schemaVersionId + signature { + type + value + identity + } + } +} +``` + +Or pass the same CID as the `cid` argument on the collection to resolve the document at that version: + +```graphql +{ + ____Transaction(cid: "bafyreibtbym4uht5dppohohg4wg66tdg4r253ws2i4wshc2gtwje6e25sy") { + _docID + blockNumber + hash + to + from + value + } +} +``` + +Because the CID is derived from the content, the document it resolves to is exactly the version that was signed. A Host that altered the data would produce a different CID. + +## Verify a whole block at once + +Signing every document individually would be slow, so Generator clients also sign per block. After writing a block's documents, the Generator client computes a Merkle root over their CIDs, signs the root, and writes a `BlockSignature` document. Snapshot signatures do the same across block ranges for faster initial sync. Verifying one block-level signature covers every primitive document in that block. The two-level Merkle structure is described in the [architecture reference](/reference/architecture/), and [Attestation](/understand/core-concepts/attestation/) explains how Host clients turn these signatures into attestation records. + +{% admonition(type="note") %} +Signatures and CIDs prove who produced your data and that it was not altered in transit. They do not prove completeness (that no matching documents were withheld from your result) or freshness (that you are seeing the latest state). Closing those gaps is roadmap work; see [Privacy](/understand/core-concepts/privacy/) for how Shinzo frames the remaining trust assumptions. +{% end %} + +## Need help + +{{ need_help(client="Host", repo_name="shinzo-host-client", repo="https://github.com/shinzonetwork/shinzo-host-client/issues") }} diff --git a/content/build/how-to/view-recipes/index.md b/content/build/how-to/view-recipes/index.md index 176b7db..b10d550 100644 --- a/content/build/how-to/view-recipes/index.md +++ b/content/build/how-to/view-recipes/index.md @@ -1,6 +1,629 @@ +++ title = "View recipes" +aliases = ["/views/examples", "/build/create-a-view/examples/"] description = "How to build common Views: decode event logs, filter by contract, decode multiple event types, transaction Views, and editing or rolling back a View." +++ -This page is coming soon. +Seven recipes for the Views people build most often, from a basic event decode to editing and rolling back a deployed View. Each recipe states its goal, shows the View components (query, SDL, lens), gives the Viewkit commands to build it, and ends with the GraphQL query you run against the result. + +If you have never built a View, work through [Create your first View](/build/tutorials/create-your-first-view/) first. When none of the prebuilt lenses does what you need, see [Write and test a custom lens](/build/how-to/write-a-lens/). + +## Primitive data + +Views query the primitive collections that Generator clients produce. All collection names are prefixed with `____`, derived from the Generator's `chain.name` and `chain.network` settings. Viewkit lets you use short names like `Log`, and the Host client auto-prefixes them at runtime. + +| Collection | Common fields | Typical use | +| --- | --- | --- | +| `Log` | `address`, `topics`, `data`, `transactionHash`, `blockNumber` | Event decoding (fungible tokens, NFTs, governance) | +| `Transaction` | `hash`, `from`, `to`, `value`, `blockNumber`, `status`, `gasUsed` | Transaction analytics | +| `Block` | `number`, `hash`, `timestamp`, `miner`, `gasUsed`, `gasLimit` | Block-level metadata | +| `AccessListEntry` | `address`, `storageKeys`, `blockNumber` | Access lists | + +There is no `Event` collection. Raw event data lives in `Log`, where `topics` holds indexed parameters and `data` holds non-indexed ones. A lens decodes those raw fields into structured output. For the full list of primitive collections, including `BlockSignature` and `SnapshotSignature`, see [Views for builders](/build/concepts/views-for-builders/#primitive-data-views-operate-on). + +## Decode event logs + +Goal: decode all `Transfer` events from fungible token contracts into structured records. This is the simplest useful View that includes a lens: it decodes raw log `topics` and `data` into named fields using an ABI. + +### Query + +```graphql +Log { address topics data transactionHash blockNumber transaction { hash from to } } +``` + +The query selects raw log fields plus the nested `transaction` relation. The `decode_log` lens uses `transaction.hash`, `transaction.from`, and `transaction.to` to populate the output's `hash`, `from`, and `to` fields. + +### SDL + +```graphql +type EventView @materialized(if: true) { + hash: String + from: String + to: String + blockNumber: Int + logAddress: String + event: String + signature: String + arguments: [String] +} +``` + +The `decode_log` lens outputs these fields: + +- `hash`, `from`, `to`: from the parent transaction. +- `blockNumber`: block the log was emitted in. +- `logAddress`: the contract that emitted the log. +- `event`: decoded event name (e.g. `"Transfer"`). +- `signature`: event signature (e.g. `"Transfer(address,address,uint256)"`). +- `arguments`: array of decoded parameters as JSON strings. + +`@materialized(if: true)` tells DefraDB to pre-compute and store the output. See [Choose materialized vs on-query](#choose-materialized-vs-on-query) for the tradeoff. + +### Lens + +| Lens | Purpose | Arguments | +| --- | --- | --- | +| `decode_log` | ABI-decode log events into named fields | `{"abi":"[{\"type\":\"event\",\"name\":\"Transfer\",...}]"}` | + +The `decode_log` lens takes an `abi` argument: a stringified JSON array of event definitions. For the `Transfer` event of a fungible token: + +```json +[{"type":"event","name":"Transfer","inputs":[{"type":"address","name":"from","indexed":true},{"type":"address","name":"to","indexed":true},{"type":"uint256","name":"value","indexed":false}]}] +``` + +### Commands + +1. Initialize the View: + + ```shell + viewkit view init event-view + ``` + +1. Add the query (raw log shape with the transaction relation): + + ```shell + viewkit view add query \ + "Log { address topics data transactionHash blockNumber transaction { hash from to } }" \ + --name event-view + ``` + +1. Add the SDL (output schema matching the `decode_log` output): + + ```shell + viewkit view add sdl \ + "type EventView @materialized(if: true) { hash: String from: String to: String blockNumber: Int logAddress: String event: String signature: String arguments: [String] }" \ + --name event-view + ``` + +1. Attach the decode lens with the Transfer ABI: + + ```shell + viewkit view add lens \ + --label "decode-transfer" \ + --url "https://raw.githubusercontent.com/shinzonetwork/wasm-bucket/main/bucket/decode_log/decode_log.wasm" \ + --args '{"abi":"[{\"type\":\"event\",\"name\":\"Transfer\",\"inputs\":[{\"type\":\"address\",\"name\":\"from\",\"indexed\":true},{\"type\":\"address\",\"name\":\"to\",\"indexed\":true},{\"type\":\"uint256\",\"name\":\"value\",\"indexed\":false}]}]"}' \ + --name event-view + ``` + +1. Inspect to confirm everything is attached: + + ```shell + viewkit view inspect event-view + ``` + +1. Test locally (optional but recommended): + + ```shell + viewkit view test event-view + ``` + +1. Deploy locally and explore in the playground: + + ```shell + viewkit view deploy event-view --target local + ``` + +### Query the result + +Once deployed, open the DefraDB Playground (URL printed in the terminal) and run: + +```graphql +{ + EventView(limit: 10, order: { blockNumber: DESC }) { + hash + from + to + blockNumber + logAddress + event + signature + arguments + } +} +``` + +This returns decoded `Transfer` events across all contracts. To narrow down to a specific token, see [Filter by contract address](#filter-by-contract-address). + +## Filter by contract address + +Goal: decode `Transfer` events from one specific contract only, such as a single token. Without a filter lens, `decode_log` processes every log on the chain. You filter the output using GraphQL queries against the `logAddress` field. + +### Query and SDL + +Same as [Decode event logs](#decode-event-logs). The query, SDL, and lens are identical. The filtering happens at query time, not at the lens level. + +### Commands + +Same as the previous recipe. Create a View named `usdc-event` with the same query, SDL, and lens. + +### Filter to one contract + +```graphql +{ + EventView( + filter: { logAddress: { _eq: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } } + order: { blockNumber: DESC } + limit: 10 + ) { + hash + from + to + blockNumber + event + signature + arguments + } +} +``` + +The `logAddress` field contains the contract address that emitted the log. Filter on it to narrow results to one contract. + +### Filter by sender or receiver + +The `from` and `to` fields come from the parent transaction, not the event's indexed parameters. To filter by the event's `from`/`to` (the actual transfer sender and receiver), use the `arguments` field. With `decode_log_str` (which serializes `arguments` as a JSON string), you can use `_like`: + +```graphql +{ + EventView( + filter: { + _and: [ + { logAddress: { _eq: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } } + { arguments: { _like: "%0x28C6c06298d514De0879A2640AB71F86b50Ce4E5%" } } + ] + } + ) { + hash + from + to + arguments + blockNumber + } +} +``` + +{% admonition(type="tip", title="decode_log vs decode_log_str") %} +`decode_log` outputs `arguments` as a JSON array (`[String]` in SDL). `decode_log_str` outputs it as a JSON string (`String` in SDL), which enables `_like` filtering in DefraDB queries. Use `decode_log_str` when you need to filter on decoded parameter values. The `_str` variant uses the same URL pattern but with `decode_log_str` in the path. Both lenses are listed in the [lens reference](/reference/components/lens/). +{% end %} + +## Decode multiple event types + +Goal: decode both `Transfer` and `Approval` events from a single contract in one View. Pass both event definitions in the ABI argument to `decode_log`. + +### Query + +```graphql +Log { address topics data transactionHash blockNumber transaction { hash from to } } +``` + +### SDL + +```graphql +type EventView @materialized(if: true) { + hash: String + from: String + to: String + blockNumber: Int + logAddress: String + event: String + signature: String + arguments: [String] +} +``` + +### Lens + +The ABI argument includes both `Transfer` and `Approval` event definitions. The `decode_log` lens matches each log's `topics[0]` against the event signature hash and decodes accordingly. + +```json +[ + {"type":"event","name":"Transfer","inputs":[{"type":"address","name":"from","indexed":true},{"type":"address","name":"to","indexed":true},{"type":"uint256","name":"value","indexed":false}]}, + {"type":"event","name":"Approval","inputs":[{"type":"address","name":"owner","indexed":true},{"type":"address","name":"spender","indexed":true},{"type":"uint256","name":"value","indexed":false}]} +] +``` + +### Commands + +```shell +# 1) initialize the View +viewkit view init erc20-events + +# 2) add the query +viewkit view add query \ + "Log { address topics data transactionHash blockNumber transaction { hash from to } }" \ + --name erc20-events + +# 3) add the SDL +viewkit view add sdl \ + "type EventView @materialized(if: true) { hash: String from: String to: String blockNumber: Int logAddress: String event: String signature: String arguments: [String] }" \ + --name erc20-events + +# 4) attach the decode lens with both Transfer and Approval in the ABI +viewkit view add lens \ + --label "decode-erc20" \ + --url "https://raw.githubusercontent.com/shinzonetwork/wasm-bucket/main/bucket/decode_log/decode_log.wasm" \ + --args '{"abi":"[{\"type\":\"event\",\"name\":\"Transfer\",\"inputs\":[{\"type\":\"address\",\"name\":\"from\",\"indexed\":true},{\"type\":\"address\",\"name\":\"to\",\"indexed\":true},{\"type\":\"uint256\",\"name\":\"value\",\"indexed\":false}]},{\"type\":\"event\",\"name\":\"Approval\",\"inputs\":[{\"type\":\"address\",\"name\":\"owner\",\"indexed\":true},{\"type\":\"address\",\"name\":\"spender\",\"indexed\":true},{\"type\":\"uint256\",\"name\":\"value\",\"indexed\":false}]}]"}' \ + --name erc20-events + +# 5) inspect, test, and deploy +viewkit view inspect erc20-events +viewkit view test erc20-events +viewkit view deploy erc20-events --target local +``` + +### Transfers only + +```graphql +{ + EventView( + filter: { + _and: [ + { logAddress: { _eq: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } } + { event: { _eq: "Transfer" } } + ] + } + limit: 10 + ) { + hash + from + to + event + arguments + blockNumber + } +} +``` + +### Approvals only + +```graphql +{ + EventView( + filter: { + _and: [ + { logAddress: { _eq: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } } + { event: { _eq: "Approval" } } + ] + } + limit: 10 + ) { + hash + from + to + event + arguments + blockNumber + } +} +``` + +The `event` field lets you distinguish between event types in the same View collection. + +## Query transactions without a lens + +Goal: expose all transactions sent to a specific contract. This View queries `Transaction` documents directly, and needs no lens because nothing is being decoded. + +### Query + +```graphql +Transaction { hash from to value blockNumber gasUsed gasPrice } +``` + +### SDL + +```graphql +type TransactionView @materialized(if: false) { + hash: String + from: String + to: String + value: String + blockNumber: Int + gasUsed: String + gasPrice: String +} +``` + +Here we use `@materialized(if: false)`: the View is computed on query, not pre-stored. This makes sense for transaction data, which is large and queried less frequently than decoded events. See [Choose materialized vs on-query](#choose-materialized-vs-on-query) for details. + +### Lens + +None. The query and SDL are sufficient. DefraDB applies the View as a virtual projection over the `Transaction` collection. + +### Commands + +```shell +# 1) initialize the View +viewkit view init transaction-view + +# 2) add the query +viewkit view add query \ + "Transaction { hash from to value blockNumber gasUsed gasPrice }" \ + --name transaction-view + +# 3) add the SDL +viewkit view add sdl \ + "type TransactionView @materialized(if: false) { hash: String from: String to: String value: String blockNumber: Int gasUsed: String gasPrice: String }" \ + --name transaction-view + +# 4) inspect, test, and deploy +viewkit view inspect transaction-view +viewkit view test transaction-view +viewkit view deploy transaction-view --target local +``` + +### Query the result + +```graphql +{ + TransactionView( + filter: { to: { _eq: "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45" } } + order: { blockNumber: DESC } + limit: 10 + ) { + hash + from + to + value + blockNumber + gasUsed + gasPrice + } +} +``` + +## Choose materialized vs on-query + +Goal: decide whether a View's output should be pre-computed and stored, or computed fresh on each query. + +The `@materialized` directive controls when the View's output is computed: + +| `@materialized` | When it computes | Query speed | Storage | Best for | +| --- | --- | --- | --- | --- | +| `if: true` | At write time. The Host pre-computes and stores results. | Fast. Data is already materialized. | Higher. The Host stores the output collection. | Frequently queried data (e.g. token transfers in a UI). | +| `if: false` | At query time. The Host computes on the fly. | Slower. Depends on data volume. | Lower. No pre-stored output. | Large datasets queried occasionally, or during development. | + +### Same View in two modes + +Materialized (pre-computed): + +```graphql +type EventView @materialized(if: true) { + hash: String + from: String + to: String + blockNumber: Int + logAddress: String + event: String + signature: String + arguments: [String] +} +``` + +On-query (virtual): + +```graphql +type EventView @materialized(if: false) { + hash: String + from: String + to: String + blockNumber: Int + logAddress: String + event: String + signature: String + arguments: [String] +} +``` + +### Switch modes + +To toggle materialization on an existing View, update the SDL: + +```shell +# switch to materialized +viewkit view add sdl \ + "type EventView @materialized(if: true) { hash: String from: String to: String blockNumber: Int logAddress: String event: String signature: String arguments: [String] }" \ + --name event-view + +# or switch to on-query +viewkit view add sdl \ + "type EventView @materialized(if: false) { hash: String from: String to: String blockNumber: Int logAddress: String event: String signature: String arguments: [String] }" \ + --name event-view +``` + +Then redeploy: + +```shell +viewkit view test event-view +viewkit view deploy event-view --target local +``` + +{% admonition(type="tip") %} +Use `@materialized(if: false)` while developing and iterating on a View. Switch to `@materialized(if: true)` once the View is stable and you need fast queries in production. +{% end %} + +## Edit and roll back a View + +Goal: modify an existing View without starting from scratch. This recipe builds on the `erc20-events` View from [Decode multiple event types](#decode-multiple-event-types) and shows the full edit lifecycle: swap a lens, inspect revisions, roll back, test, and redeploy. + +### Starting point + +Assume you already have `erc20-events` deployed with: + +- Query: `Log { address topics data transactionHash blockNumber transaction { hash from to } }` +- SDL: `type EventView @materialized(if: true) { hash: String from: String to: String blockNumber: Int logAddress: String event: String signature: String arguments: [String] }` +- Lens: `decode-erc20` (`decode_log`, Transfer + Approval ABI) + +### Swap the lens + +Remove the old lens, then add a new one with an updated ABI that decodes three event types (the third `Transfer` variant uses a `tokenId` parameter instead of `value`): + +```shell +# remove the old lens +viewkit view remove lens \ + --label "decode-erc20" \ + --name erc20-events + +# add a new lens with three event types +viewkit view add lens \ + --label "decode-multi" \ + --url "https://raw.githubusercontent.com/shinzonetwork/wasm-bucket/main/bucket/decode_log/decode_log.wasm" \ + --args '{"abi":"[{\"type\":\"event\",\"name\":\"Transfer\",\"inputs\":[{\"type\":\"address\",\"name\":\"from\",\"indexed\":true},{\"type\":\"address\",\"name\":\"to\",\"indexed\":true},{\"type\":\"uint256\",\"name\":\"value\",\"indexed\":false}]},{\"type\":\"event\",\"name\":\"Approval\",\"inputs\":[{\"type\":\"address\",\"name\":\"owner\",\"indexed\":true},{\"type\":\"address\",\"name\":\"spender\",\"indexed\":true},{\"type\":\"uint256\",\"name\":\"value\",\"indexed\":false}]},{\"type\":\"event\",\"name\":\"Transfer\",\"inputs\":[{\"type\":\"address\",\"name\":\"from\",\"indexed\":true},{\"type\":\"address\",\"name\":\"to\",\"indexed\":true},{\"type\":\"uint256\",\"name\":\"tokenId\",\"indexed\":true}]}]"}' \ + --name erc20-events +``` + +### Inspect the revision history + +Every `add` and `remove` creates a new revision. To see the full history: + +```shell +viewkit view inspect erc20-events --verbose +``` + +This shows the current state (query, SDL, lenses) and all past revisions, each with a version number. + +### Roll back if something went wrong + +If the updated ABI doesn't work as expected, revert to the previous version: + +```shell +# roll back to the most recent previous version +viewkit view rollback erc20-events +``` + +Or roll back to a specific version: + +```shell +viewkit view rollback erc20-events --version 3 +``` + +### Test and redeploy + +```shell +# validate the updated View compiles +viewkit view test erc20-events + +# deploy locally to verify in the playground +viewkit view deploy erc20-events --target local + +# once verified, deploy to the public testnet +viewkit view deploy erc20-events --target devnet --rpc http://testnet.shinzo.network:8545/ +``` + +{% admonition(type="note") %} +The CLI's network target is called `devnet`, but pointed at `http://testnet.shinzo.network:8545/` it deploys to the public testnet. Registration is an on-chain transaction, so the wallet you deploy from needs testnet tokens from the [faucet](https://faucet.shinzo.network/). +{% end %} + +### Delete a View + +To remove a View bundle from your local machine entirely: + +```shell +viewkit view delete erc20-events +``` + +This deletes the local bundle. It does not remove a View that has already been deployed to the network. On-chain registrations are permanent. To update a deployed View, deploy a new version with the same name. + +## Query a deployed View + +Goal: run GraphQL queries against a deployed View's output collection. These examples assume the `erc20-events` View from [Decode multiple event types](#decode-multiple-event-types) is deployed and receiving data. + +### Get the latest 10 events + +```graphql +{ + EventView( + order: { blockNumber: DESC } + limit: 10 + ) { + hash + from + to + blockNumber + logAddress + event + signature + arguments + } +} +``` + +### Filter by contract and event type + +```graphql +{ + EventView( + filter: { + _and: [ + { logAddress: { _eq: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } } + { event: { _eq: "Transfer" } } + ] + } + limit: 10 + ) { + hash + from + to + arguments + blockNumber + } +} +``` + +### Filter by block range + +```graphql +{ + EventView( + filter: { blockNumber: { _geq: 19540000 } } + ) { + hash + from + to + event + blockNumber + } +} +``` + +### Filter by transaction hash + +```graphql +{ + EventView( + filter: { hash: { _eq: "0xabc123..." } } + ) { + hash + from + to + event + signature + arguments + blockNumber + } +} +``` + +For the full list of Viewkit commands and GraphQL filter operators, see the [Viewkit reference](/reference/components/viewkit/). For the prebuilt lens catalog and how to chain lenses, see the [lens reference](/reference/components/lens/). For more query patterns, see [Query data](/build/how-to/query-data/). For troubleshooting and common errors, see the [FAQ](/run/operations/troubleshooting/). + +## Need help + +{{ need_help(client="Viewkit", repo_name="shinzo-view-creator", repo="https://github.com/shinzonetwork/shinzo-view-creator/issues") }} diff --git a/content/build/how-to/write-a-lens/index.md b/content/build/how-to/write-a-lens/index.md index 77bb403..31e936d 100644 --- a/content/build/how-to/write-a-lens/index.md +++ b/content/build/how-to/write-a-lens/index.md @@ -1,6 +1,178 @@ +++ title = "Write and test a custom lens" -description = "How to author a WebAssembly lens with the AssemblyScript SDK, test it locally, and attach it to a View." +description = "How to author a WebAssembly lens with the Rust SDK, test it locally, and attach it to a View." +++ -This page is coming soon. +Most Views never need a custom lens. The prebuilt lenses in the wasm-bucket cover log decoding and function-call decoding, and the [lens reference](/reference/components/lens/) lists them with their arguments. Write your own only when the transform you need does not exist. + +A lens is a WebAssembly module that sits between a View's query and its output. Every Host client runs the same lens over the same documents through LensVM, so the output has to be identical everywhere. For each input document, the lens returns a transformed document, or drops the document entirely. + +## Scaffold the lens + +The wasm-bucket lenses are written in Rust against the `lens_sdk` crate, and that SDK is the path documented here. Create a new library crate: + +```toml +[package] +name = "drop_old_logs" +version = "0.1.0" +edition = "2018" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +serde_json = "1.0" +lens_sdk = "^0.7.0" +``` + +The lens below drops every log emitted before a fixed block height and passes everything else through unchanged. It shows the three things every lens has: an `alloc` export so the host runtime can hand over memory, a `transform` export that does the work, and the tagged-buffer convention (`JSON_TYPE_ID`, `EOS_TYPE_ID`, `ERROR_TYPE_ID`) that tells the runtime what came back. + +```rust +use std::collections::HashMap; +use std::error; + +use lens_sdk::option::StreamOption::{EndOfStream, None, Some}; +use lens_sdk::StreamOption; +use serde_json::Value; + +#[link(wasm_import_module = "lens")] +extern "C" { + fn next() -> *mut u8; +} + +const MIN_BLOCK: i64 = 19_000_000; + +#[no_mangle] +pub extern "C" fn alloc(size: usize) -> *mut u8 { + lens_sdk::alloc(size) +} + +#[no_mangle] +pub extern "C" fn transform() -> *mut u8 { + match try_transform() { + Ok(Some(json)) => tagged_mem(lens_sdk::JSON_TYPE_ID, &json), + Ok(None) => lens_sdk::nil_ptr(), + Ok(EndOfStream) => tagged_mem(lens_sdk::EOS_TYPE_ID, &[]), + Err(e) => tagged_mem(lens_sdk::ERROR_TYPE_ID, e.to_string().as_bytes()), + } +} + +fn try_transform() -> Result>, Box> { + let ptr = unsafe { next() }; + let doc = match lens_sdk::try_from_mem::>(ptr)? { + Some(v) => v, + None => return Ok(None), + EndOfStream => return Ok(EndOfStream), + }; + + if below_min_block(&doc) { + return Ok(None); // a nil return drops the document + } + + Ok(Some(serde_json::to_vec(&doc)?)) +} + +fn below_min_block(doc: &HashMap) -> bool { + let block_number = doc.get("blockNumber").and_then(|v| v.as_i64()).unwrap_or(0); + block_number < MIN_BLOCK +} + +// The same tagged-buffer layout the wasm-bucket lenses use. +fn tagged_mem(type_id: i8, data: &[u8]) -> *mut u8 { + let total = 1 + 4 + data.len(); + let ptr = lens_sdk::alloc(total); + unsafe { + *ptr = type_id as u8; + let len_bytes = (data.len() as u32).to_le_bytes(); + std::ptr::copy_nonoverlapping(len_bytes.as_ptr(), ptr.add(1), 4); + if !data.is_empty() { + std::ptr::copy_nonoverlapping(data.as_ptr(), ptr.add(5), data.len()); + } + } + ptr +} +``` + +Two things to notice. The `next()` function is imported from the `lens` module: the runtime calls `transform`, and `transform` pulls the next input document by calling `next()`. And dropping a document is just returning a nil pointer, which is how filters are expressed as lenses. + +Lenses that need arguments, like the `abi` parameter of `decode_log`, add a `set_param` export that deserializes a parameters struct once at load time. The [decode_log source](https://github.com/shinzonetwork/wasm-bucket/tree/main/bucket/decode_log) is the best real-world reference for that pattern. + +## Keep the logic testable + +The FFI shell around a lens is tedious to test, so keep it thin. Everything in `try_transform` after the deserialization is ordinary Rust: put the decision in a pure function like `below_min_block` and cover it with `cargo test`. + +```rust +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn drops_old_logs() { + let mut doc = HashMap::new(); + doc.insert("blockNumber".to_string(), Value::from(18_000_000)); + assert!(below_min_block(&doc)); + } + + #[test] + fn keeps_new_logs() { + let mut doc = HashMap::new(); + doc.insert("blockNumber".to_string(), Value::from(20_000_000)); + assert!(!below_min_block(&doc)); + } +} +``` + +## Build the module + +Compile to WebAssembly with the `wasm32-unknown-unknown` target: + +```shell +rustup target add wasm32-unknown-unknown +cargo build --target wasm32-unknown-unknown --release +``` + +The module lands at `target/wasm32-unknown-unknown/release/drop_old_logs.wasm`. + +{% admonition(type="note") %} +Hosts run lenses through LensVM, and the runtime depends on where the lens executes: production Host clients use wazero, while Viewkit's local testing uses wasmer. Both enforce the same rules, so a lens that is deterministic in one is deterministic in the other. +{% end %} + +## Attach it to a View and test the whole thing + +A local build attaches to a View with `--path` instead of `--url`: + +```shell +viewkit view add lens \ + --label "drop-old-logs" \ + --path ./target/wasm32-unknown-unknown/release/drop_old_logs.wasm \ + --args '{}' \ + --name my-view +``` + +Then run the full-View check, which spins up a local node, applies the lens, and validates the output: + +```shell +viewkit view test my-view +viewkit view deploy my-view --target local +``` + +If the View compiles and the playground shows the filtered rows, the lens is doing its job. From here the usual cycle applies: edit, rebuild, `viewkit view test`, redeploy. + +## Respect the determinism rules + +Every Host client runs your lens over the same documents, and the results are compared. If two Hosts disagree, something is wrong, so a lens must produce exactly the same output on every run. That rules out: + +- Reading the system clock or generating random numbers. +- Making network calls. +- Depending on file system state. +- Using floating-point arithmetic, which can vary across WASM runtimes. + +The hardcoded `MIN_BLOCK` in the example is deliberate: a block height read from the document is deterministic, while "logs older than 24 hours" is not, because it depends on the clock. + +## The AssemblyScript alternative + +Rust is the preferred path for production lenses: it has the best tooling and optimization, with output around 200 to 300 KB. If you would rather write something closer to TypeScript, the AssemblyScript SDK produces noticeably smaller modules, around 73 KB, which means less overhead when Host clients download the View bundle. Both SDKs live in the [sourcenetwork/lens](https://github.com/sourcenetwork/lens) repo, and the [lens reference](/reference/components/lens/) has the details. + +## Need help + +{{ need_help(client="Viewkit", repo_name="shinzo-view-creator", repo="https://github.com/shinzonetwork/shinzo-view-creator/issues") }} diff --git a/content/build/query-data/index.md b/content/build/query-data/index.md deleted file mode 100644 index 4aa6562..0000000 --- a/content/build/query-data/index.md +++ /dev/null @@ -1,347 +0,0 @@ -+++ -title = "Query data" -aliases = ["/hosts/examples"] -description = "GraphQL query examples and patterns for querying indexed data through a Shinzo Host." -+++ - -This page lists common GraphQL query examples for indexed chain data. The examples focus on blocks, transactions, attestations, signatures, and document navigation using DocIDs and CIDs. - -{% admonition(type="note") %} -Collection names are prefixed with `____`, derived from the `chain.name` and `chain.network` settings of the Generator client that indexed the data (for example `____Block` or `Optimism__Mainnet__Block`). The examples below use the `____` placeholder. Substitute the prefix that matches your chain. See the [chain config](/run/run-a-generator/config-reference#chain) for details. -{% end %} - -## Querying a block with nested data - -Fetch a single block with nested sub-documents. - -```graphql -{ - ____Block(limit:1){ - _docID - number - timestamp - hash - nonce - difficulty - size - stateRoot - gasUsed - gasLimit - baseFeePerGas - logsBloom - uncles - sha3Uncles - receiptsRoot - parentHash - extraData - miner - difficulty - totalDifficulty - transactions{ - hash - blockHash - block_id - _docID - # ...additional fields - logs{ - blockHash - transactionHash - address - topics - data - # ...additional fields - } - accessList{ - storageKeys - address - transaction_id - # ...additional fields - } - } - } -} -``` - -## Blocks with signatures (verifiability) - -Verify who signed a block record and inspect the cryptographic metadata. - -```graphql -{ - ____Block(limit: 10, order: {number: DESC}) { - number - _docID - _version { - cid - signature { - identity - value - type - } - } - } -} -``` - -## Fetching a document by DocID - -Retrieve an exact document when you already know its `_docID`. - -```graphql -query { - ____Block(docID: ) { - _docID - number - _count(transactions:{}) - hash - transactions(order: {transactionIndex: DESC}) { - transactionIndex - _docID - } - } -} -``` - -## Attestations and document navigation - -Attestation records link documents to one or more CIDs. These CIDs can then be used to navigate to commit metadata or directly to the underlying document. - -### AttestationRecord - -```graphql -{ - ____AttestationRecord(limit:10){ - attested_doc - source_doc - CIDs - _docID - doc_type - } -} -``` - -#### Response - -```json -[..., { - "CIDs": [ - "bafyreibtbym4uht5dppohohg4wg66tdg4r253ws2i4wshc2gtwje6e25sy" - ], - "_docID": "bae-00000035-bd9b-5938-a55f-3a477dac226a", - "attested_doc": "bae-25fb059c-f232-5305-8a5d-0162f01e43e6", - "doc_type": "____Transaction", - "source_doc": "bae-25fb059c-f232-5305-8a5d-0162f01e43e6" -},...] -``` - -### CID to commit details - -Given a CID from an attestation record, you can query commit-level metadata and signatures. - -```graphql -{ - _commits( - cid:"bafyreibtbym4uht5dppohohg4wg66tdg4r253ws2i4wshc2gtwje6e25sy" - ){ - cid - docID - fieldName - schemaVersionId - signature{ - type - value - identity - } - } -} -``` - -#### Response - -```json -{ - "data": { - "_commits": [ - { - "cid": "bafyreibtbym4uht5dppohohg4wg66tdg4r253ws2i4wshc2gtwje6e25sy", - "docID": "bae-25fb059c-f232-5305-8a5d-0162f01e43e6", - "fieldName": "_C", - "schemaVersionId": "bafyreiagteeodcsrofk3s4fhubdi7jdzjeovhvpx4yayxkcxw2gm4zlcru", - "signature": { - "identity": "0348621aed3cb78ade074e86a3d650dfdfad0c110b274c0633b331d1b0a41ddd99", - "type": "ES256K", - "value": "MEUCIQCjfh3m0RNv4j094aW5YPEeF+GCMFWEGy0hiAcga7HKbQIgc54AV7WSdXZVyGH7jOuLcXJ6w5fDQSUdrlzgZhDkBTw=" - } - } - ] - } -} -``` - -### CID to document - -The same CID can be used to directly resolve the document itself. - -```graphql -{ - ____Transaction(cid:"bafyreibtbym4uht5dppohohg4wg66tdg4r253ws2i4wshc2gtwje6e25sy"){ - _docID - block_id - blockHash - blockNumber - hash - to - from - transactionIndex - value - # ... other fields - } -} -``` - -#### Response - -```json -{ - "data": { - "____Transaction": [ - { - "_docID": "bae-25fb059c-f232-5305-8a5d-0162f01e43e6", - "blockHash": "0x9ea35b3bd9e71c57617cc30394b22f607b735f2eea7a0db974cf02ad54de98fb", - "blockNumber": 23902272, - "block_id": "bae-91bd3f16-ccb1-5c35-b098-45672ee6fd48", - "from": "0x654a6BCe2C6F0aF68eAdCFEaD06bB49C398B3F98", - "hash": "0x61b79fc417ef183e1798681c59481410dd79f919d11806a6e7e77ebd0a744f78", - "to": "0x677f857da5e7C42b823655290cc40ff401e138D3", - "transactionIndex": 130, - "value": "1000000000" - } - ] - } -} -``` - -### From CID to document directly - -```graphql -{ - ____Transaction(cid:"bafyreibtbym4uht5dppohohg4wg66tdg4r253ws2i4wshc2gtwje6e25sy"){ - _docID - block_id - blockHash - blockNumber - hash - to - from - transactionIndex - value - # ... other fields - } -} -``` - -#### Response - -```json -{ - "data": { - "____Transaction": [ - { - "_docID": "bae-25fb059c-f232-5305-8a5d-0162f01e43e6", - "blockHash": "0x9ea35b3bd9e71c57617cc30394b22f607b735f2eea7a0db974cf02ad54de98fb", - "blockNumber": 23902272, - "block_id": "bae-91bd3f16-ccb1-5c35-b098-45672ee6fd48", - "from": "0x654a6BCe2C6F0aF68eAdCFEaD06bB49C398B3F98", - "hash": "0x61b79fc417ef183e1798681c59481410dd79f919d11806a6e7e77ebd0a744f78", - "to": "0x677f857da5e7C42b823655290cc40ff401e138D3", - "transactionIndex": 130, - "value": "1000000000" - } - ] - } -} -``` - -## DocID-based queries - -```graphql -{ - ____Transaction(docID:"bae-25fb059c-f232-5305-8a5d-0162f01e43e6"){ - _docID - block_id - blockHash - blockNumber - hash - to - from - transactionIndex - value - # ... other fields - } -} -``` - -#### Response - -```json -{ - "data": { - "____Transaction": [ - { - "_docID": "bae-25fb059c-f232-5305-8a5d-0162f01e43e6", - "blockHash": "0x9ea35b3bd9e71c57617cc30394b22f607b735f2eea7a0db974cf02ad54de98fb", - "blockNumber": 23902272, - "block_id": "bae-91bd3f16-ccb1-5c35-b098-45672ee6fd48", - "from": "0x654a6BCe2C6F0aF68eAdCFEaD06bB49C398B3F98", - "hash": "0x61b79fc417ef183e1798681c59481410dd79f919d11806a6e7e77ebd0a744f78", - "to": "0x677f857da5e7C42b823655290cc40ff401e138D3", - "transactionIndex": 130, - "value": "1000000000" - } - ] - } -} -``` - -## Filters ordering and limits - -### Number of transactions in a specific block - -```graphql -query { - ____Block( filter: { number: { _eq: 23901130 } } ){ - _docID - number - hash - receiptsRoot - size - gasUsed - transactions( - limit: 1, - filter: { blockNumber: { _eq: 23901130 } } - order: { transactionIndex: DESC } - ) { - transactionIndex # highest index within the block / +1 to get tx count - } - } -} -``` - -The total transaction count is `highest transactionIndex + 1`. - -## Block with transaction count - -```graphql -query { - ____Block(limit:10) { - _docID - number - hash - _count(transactions:{}) - } -} -``` - -## Need help - -{{ need_help(client="Host", repo_name="shinzo-host-client", repo="https://github.com/shinzonetwork/shinzo-host-client/issues") }} diff --git a/content/reference/components/viewkit/index.md b/content/reference/components/viewkit/index.md index 93baa8a..eeea336 100644 --- a/content/reference/components/viewkit/index.md +++ b/content/reference/components/viewkit/index.md @@ -37,14 +37,13 @@ When querying a deployed view's output collection, DefraDB supports these filter | Operator | Meaning | Example | | --- | --- | --- | -| `_eq` | Equal | `{ logAddress: { _eq: "0x..." } }` | -| `_ne` | Not equal | `{ event: { _ne: "Approval" } }` | -| `_gt` / `_gte` | Greater than / greater than or equal | `{ blockNumber: { _gte: 19540000 } }` | -| `_lt` / `_lte` | Less than / less than or equal | `{ blockNumber: { _lte: 19541000 } }` | +| `_eq` / `_neq` | Equal / not equal | `{ logAddress: { _eq: "0x..." } }` | +| `_gt` / `_geq` | Greater than / greater than or equal | `{ blockNumber: { _geq: 19540000 } }` | +| `_lt` / `_leq` | Less than / less than or equal | `{ blockNumber: { _leq: 19541000 } }` | +| `_in` / `_nin` | In / not in a list of values | `{ event: { _in: ["Transfer", "Approval"] } }` | | `_and` | Logical AND | `{ _and: [{ logAddress: { _eq: "0x..." } }, { event: { _eq: "Transfer" } }] }` | | `_or` | Logical OR | `{ _or: [{ from: { _eq: "0x..." } }, { to: { _eq: "0x..." } }] }` | -| `_like` | Substring match (strings) | `{ arguments: { _like: "%0xAddress%" } }` | -| `_any` | Any element in array matches | `{ topics: { _any: { _eq: "0xddf252..." } } }` | +| `_like` / `_ilike` | Substring match, case-sensitive / case-insensitive (strings) | `{ arguments: { _like: "%0xAddress%" } }` | ## What happens during deploy From 5c5208e789229992008368bcd0b73b8409b89bb8 Mon Sep 17 00:00:00 2001 From: Johnny <9611008+johnnymatthews@users.noreply.github.com> Date: Sat, 5 Sep 2026 12:40:40 +0100 Subject: [PATCH 08/11] Re-adds missing page. Updates links. --- .../concepts/views-for-builders/index.md | 4 +- content/build/create-a-view/index.md | 474 ------------------ .../tutorials/create-your-first-view/index.md | 414 ++++++++++++++- content/reference/components/lens/index.md | 2 +- content/reference/components/viewkit/index.md | 2 +- .../run/operations/troubleshooting/index.md | 2 +- content/run/run-a-host/install/index.md | 4 +- .../understand/core-concepts/pools/index.md | 2 +- static/_redirects | 12 +- templates/macros/footer.html | 2 +- 10 files changed, 428 insertions(+), 490 deletions(-) delete mode 100644 content/build/create-a-view/index.md diff --git a/content/build/concepts/views-for-builders/index.md b/content/build/concepts/views-for-builders/index.md index 0f6b1eb..f6fedf6 100644 --- a/content/build/concepts/views-for-builders/index.md +++ b/content/build/concepts/views-for-builders/index.md @@ -77,11 +77,11 @@ The `Log` collection is the primary source for event-based Views. There is no `E Viewkit is a local-first CLI: you initialize a view bundle, incrementally add or update its query, SDL, and lenses, test it locally against a throwaway DefraDB instance, then deploy to local or devnet. Strong defaults, explicit versioning, and deterministic behavior mean developers focus on data semantics and transformations; Shinzo handles execution, distribution, and verification. -For the hands-on walkthrough, see [Create a View](/build/create-a-view/). For the full command list, filter operators, and deploy internals, see the [Viewkit reference](/reference/components/viewkit/). +For the hands-on walkthrough, see [Create your first View](/build/tutorials/create-your-first-view/). For the full command list, filter operators, and deploy internals, see the [Viewkit reference](/reference/components/viewkit/). ## Where to go next -- [Create a View](/build/create-a-view/): step-by-step guide to building and deploying your first view. +- [Create your first View](/build/tutorials/create-your-first-view/): step-by-step tutorial for building and deploying your first View. - [Pools](/understand/core-concepts/pools/): how developer demand for a View meets the Hosts that serve it. - [Build an app](/build/build-an-app/): use the app-sdk to subscribe to Views and query data locally. - [Query data](/build/query-data/): GraphQL query patterns for deployed Views. diff --git a/content/build/create-a-view/index.md b/content/build/create-a-view/index.md deleted file mode 100644 index b53fa07..0000000 --- a/content/build/create-a-view/index.md +++ /dev/null @@ -1,474 +0,0 @@ -+++ -title = "Create a View" -aliases = ["/views/quickstart", "/views/install"] -description = "Build and deploy your first Shinzo View with Viewkit — from install to local testing to devnet publishing." -+++ -Viewkit is a CLI tool that helps you initialize, manage, and publish Shinzo views. In this guide we're going to build and install the `viewkit` executable, create a view, and publish it to the testnet. If you'd rather work from a browser UI instead of the CLI, [Shinzo Studio](https://studio.shinzo.network/) covers the same create → deploy → query flow for Views. - -## Prerequisites - -- Git -- Make -- Go 1.25 - -## Setup - -1. Make sure you've got all the prerequisites installed properly: - - ```shell - git --version && make --version && go version - ``` - - ```output - git version 2.43.0 - GNU Make 4.3 - [...] - go version go1.25.12 linux/arm64 - ``` - -1. Clone the repository: - - ```shell - git clone https://github.com/shinzonetwork/shinzo-view-creator.git - cd shinzo-view-creator - ``` - -1. Build the Viewkit binary: - - ```shell - make build - ``` - - You should see a `build` directory: - -1. Run Viewkit: - - ```shell - ./build/viewkit --help - ``` - - ```output - Viewkit helps you initialize, manage, and publish Shinzo views through a simple CLI interface. - - Usage: - viewkit [command] - - [...] - ``` - -1. Move the `viewkit` executable somewhere reasonable (optional): - - ```shell - sudo mv ./build/viewkit /usr/local/bin - ``` - - Now you can run `viewkit` from anywhere. - -## Wasmer runtime - -Viewkit can execute WebAssembly lenses locally to validate and preview them. - -Under the hood, it uses `wasmer-go`, which depends on a native dynamic library (`libwasmer.dylib`). If your local system cannot find that library, any command that touches lenses will fail with an error like: - -```plaintext -image not found -library not loaded: libwasmer.dylib -``` - -1. Move back into the shinzo-view-creator repo if you moved out of it: - - ```shell - cd shinzo-view-creator - ``` - -1. Install the Wasmer Go module: - - ```shell - go get github.com/wasmerio/wasmer-go@v1.0.4 - ``` - - ```output - go: downloading github.com/wasmerio/wasmer-go v1.0.4 - go: added github.com/wasmerio/wasmer-go v1.0.4 - ``` - - This makes `wasmer-go` and its packaged native libraries available in your `GOPATH`. - -### Environment variables - -We need to set three new environment variables: - -- `WASMER_ROOT`: points to the directory where `libwasmer.dylib` lives. -- `WASMER_LIB_PATH`: used by `wasmer-go` to find the dynamic library. -- `DYLD_LIBRARY_PATH`: MacOS-specific dynamic loader search path. We prepend `WASMER_ROOT` so the loader can find `libwasmer.dylib` when `viewkit` starts. - -1. Append these lines to your shell's RC file. - - MacOS: - - ```shell - echo 'export WASMER_ROOT="$(go env GOPATH)/pkg/mod/github.com/wasmerio/wasmer-go@v1.0.4/wasmer/packaged/lib/darwin-aarch64"' >> ~/.zshrc - echo 'export WASMER_LIB_PATH="$WASMER_ROOT"' >> ~/.zshrc - echo 'export DYLD_LIBRARY_PATH="$WASMER_ROOT:$DYLD_LIBRARY_PATH"' >> ~/.zshrc - ``` - - Linux: - - ```shell - echo 'export WASMER_ROOT="$(go env GOPATH)/pkg/mod/github.com/wasmerio/wasmer-go@v1.0.4/wasmer/packaged/lib/linux-amd64"' >> ~/.zshrc - echo 'export WASMER_LIB_PATH="$WASMER_ROOT"' >> ~/.zshrc - echo 'export LD_LIBRARY_PATH="$WASMER_ROOT:$LD_LIBRARY_PATH"' >> ~/.zshrc - ``` - -1. Reload your shell configuration: - - ```shell - source ~/.zshrc - ``` - -1. Verify that the variables are set: - - ```shell - echo "$WASMER_ROOT" - ls "$WASMER_ROOT" - ``` - - ```output - /home/user/go/pkg/mod/github.com/wasmerio/wasmer-go@v1.0.4/wasmer/packaged/lib/linux-amd64 - dummy.go libwasmer.so - ``` - -{% admonition(type="warning") %} -If `libwasmer.dylib` is missing, re-run the `go get` step and ensure `go env GOPATH` returns a valid path. -{% end %} - -## Create a view - -Now that everything is set up, we can start creating and deploying views. - -1. Initialize the view bundle: - - ```shell - viewkit view init testdeploy - ``` - - ```output - 📄 View: testdeploy - 🔍 Query: - 📐 SDL: - 🔧 Lenses: - - (empty) - - 🗂 Metadata: - - Version: 0 - - Total: 0 - - Created At: 2026-07-09 09:34:27 +0000 UTC - - Updated At: 2026-07-09 09:34:27 +0000 UTC - ``` - - This: - - - Creates a new view bundle called `testdeploy` on disk. - - Registers internal metadata for queries, SDL, lenses, and versions. - -1. Inspect the bundle: - - ```shell - viewkit view inspect testdeploy - ``` - - ```output - 📄 View: testdeploy - 🔍 Query: - 📐 SDL: - 🔧 Lenses: - - (empty) - - 🗂 Metadata: - - Version: 0 - - Total: 0 - - Created At: 2026-07-09 09:34:27 +0000 UTC - - Updated At: 2026-07-09 09:34:27 +0000 UTC - ``` - -1. Next we're going to add a query (raw ingest shape). First, define the raw data shape to ingest, e.g. raw event logs: - - ```shell - viewkit view add query \ - "Log {address topics data transactionHash blockNumber}" \ - --name testdeploy - ``` - - ```output - 📄 View: testdeploy - 🔍 Query: - Log {address topics data transactionHash blockNumber} - - 📐 SDL: - 🔧 Lenses: - - (empty) - - 🗂 Metadata: - - Version: 1 - - Total: 1 - - Created At: 2026-07-09 09:34:27 +0000 UTC - - Updated At: 2026-07-09 09:36:32 +0000 UTC - ``` - - This tells Viewkit that `testdeploy` will ingest `Log` objects with the specified fields. - -1. Then, check: - - ```shell - viewkit view inspect testdeploy - ``` - - ```output - 📄 View: testdeploy - 🔍 Query: - Log {address topics data transactionHash blockNumber} - - 📐 SDL: - 🔧 Lenses: - - (empty) - - 🗂 Metadata: - - Version: 1 - - Total: 1 - - Created At: 2026-07-09 09:34:27 +0000 UTC - - Updated At: 2026-07-09 09:36:32 +0000 UTC - ``` - -1. Add an SDL to describe how the data is modeled/exposed: - - ```shell - viewkit view add sdl \ - "type FilteredAndDecodedLogs @materialized(if: false) {transactionHash: String}" \ - --name testdeploy - ``` - - ```output - 📄 View: testdeploy - 🔍 Query: - Log {address topics data transactionHash blockNumber} - - 📐 SDL: - type FilteredAndDecodedLogs @materialized(if: false) {transactionHash: String} - - 🔧 Lenses: - - (empty) - - 🗂 Metadata: - - Version: 2 - - Total: 2 - - Created At: 2026-07-09 09:34:27 +0000 UTC - - Updated At: 2026-07-09 09:37:24 +0000 UTC - ``` - - {% admonition(type="note") %} -- `@materialized(if: false)`: treat this as a virtual type, not a persisted table. -- `transactionHash: String`: minimal example field; real views will define more fields. - {% end %} - -1. Inspect the view again: - - ```shell - viewkit view inspect testdeploy - # now shows both query and SDL - ``` - - ```output - 📄 View: testdeploy - 🔍 Query: - Log {address topics data transactionHash blockNumber} - - 📐 SDL: - type FilteredAndDecodedLogs @materialized(if: false) {transactionHash: String} - - 🔧 Lenses: - - (empty) - - 🗂 Metadata: - - Version: 2 - - Total: 2 - - Created At: 2026-07-09 09:34:27 +0000 UTC - - Updated At: 2026-07-09 09:37:24 +0000 UTC - ``` - - It now shows both the query and the SDL. - -1. Attach a WebAssembly lens that decodes event logs using an ABI. These are the flags we're using: - - - `--args`: JSON passed to the lens (here, an ABI definition for the ERC-20 `Transfer` event). - - `--label "decode"`: human-readable label for the lens. - - `--url`: remote URL of the `.wasm` binary. - - `--name testdeploy`: attaches this lens to the `testdeploy` view. - - ```shell - viewkit view add lens \ - --args '{"abi":"[{\"type\":\"event\",\"name\":\"Transfer\",\"inputs\":[{\"type\":\"address\",\"name\":\"from\",\"indexed\":true},{\"type\":\"address\",\"name\":\"to\",\"indexed\":true},{\"type\":\"uint256\",\"name\":\"value\",\"indexed\":false}]}]"}' \ - --label "decode" \ - --url "https://raw.githubusercontent.com/shinzonetwork/wasm-bucket/main/bucket/decode_log/decode_log.wasm" \ - --name testdeploy - ``` - - ```output - 📄 View: testdeploy - 🔍 Query: - Log {address topics data transactionHash blockNumber} - - 📐 SDL: - type FilteredAndDecodedLogs @materialized(if: false) {transactionHash: String} - - 🔧 Lenses: - - decode (assets/decode.wasm) - Arguments: - abi: [{"type":"event","name":"Transfer","inputs":[{"type":"address","name":"from","indexed":true},{"type":"address","name":"to","indexed":true},{"type":"uint256","name":"value","indexed":false}]}] - - 🗂 Metadata: - - Version: 3 - - Total: 3 - - Created At: 2026-07-09 09:34:27 +0000 UTC - - Updated At: 2026-07-09 09:39:11 +0000 UTC - ``` -1. Inspect the view again: - - ```shell - viewkit view inspect testdeploy - # you should now see: - # - query - # - SDL - # - lens "decode" - ``` - - You should now see the query, SDL, and the `decode` lens: - - ```output - 📄 View: testdeploy - 🔍 Query: - Log {address topics data transactionHash blockNumber} - - 📐 SDL: - type FilteredAndDecodedLogs @materialized(if: false) {transactionHash: String} - - 🔧 Lenses: - - decode (assets/decode.wasm) - Arguments: - abi: [{"type":"event","name":"Transfer","inputs":[{"type":"address","name":"from","indexed":true},{"type":"address","name":"to","indexed":true},{"type":"uint256","name":"value","indexed":false}]}] - - 🗂 Metadata: - - Version: 3 - - Total: 3 - - Created At: 2026-07-09 09:34:27 +0000 UTC - - Updated At: 2026-07-09 09:39:11 +0000 UTC - ``` - -{% admonition(type="tip") %} -If you see `libwasmer.dylib` / "image not found" errors, revisit the Wasmer setup. -{% end %} - -1. Before deploying, validate that your view builds and compiles successfully: - - ```shell - viewkit view test testdeploy - ``` - - This spins up a temporary local DefraDB instance, applies your schema, runs the lens, and checks that everything compiles. If it passes, your view is ready to deploy. - -## Create a deployment key - -You need a wallet to sign deployments to `devnet`. - -1. Generate a new one: - - ```shell - viewkit wallet generate - ``` - - ```output - ✅ Wallet generated - Mnemonic: document grass code lawn erosion climb people sunset three blame balcony story script hip soup lesson resemble above quiz acid dust salmon plane - Address: 0x2e4150993E841b38f4780BC158A7dA0d62E22ec9 - ``` - -1. Treat this wallet like any other secret or asset: - - - Do not commit it to Git. - - Do not paste the mnemonic in public places. - - Store it securely. - -## Deploy locally - -The recommended flow is to deploy locally first, verify the view in a Playground, then deploy to a shared network. - -1. Deploy locally: - - ```shell - viewkit view deploy testdeploy --target local - ``` - - ```output - 🚀 DefraDB is running on port 9181 - ⏳ Waiting for DefraDB to boot up... - ✅ DefraDB booted up - ⏳ Applying Schemas ... - ✅ Schema Applied - ⏳ Data Inserting... - ✅ Data Inserted Successfully - ✅ Applying View ... - ✅ View Successfully Applied - 🧪 Visit the DefraDB GraphQL Playground at http://127.0.0.1:9181/ - 📦 Press Ctrl+C to stop... - ``` - - Here's what's happening: - - 1. A local DefraDB instance is started (port shown in the logs). - 1. Schemas for your view are applied. - 1. Any seed data (if configured) is inserted. - 1. The view is applied. - 1. A DefraDB GraphQL Playground URL is printed. - -### Use the DefraDB GraphQL Playground - -This section is optional, but it's a good idea to check the View within the built-in GraphQL Playground. - -1. Open the displayed URL in your browser, usually [127.0.0.1:9181](http://127.0.0.1:9181/). -1. You should see a GraphQL Playground. -1. Within this Playground you can: - - Inspect the schema (e.g. see `FilteredAndDecodedLogs`). - - Run test queries against your local view. - - Verify that your lens is filtering logs as expected. - -For example, you can run a query like: - -```graphql -{ - filteredAndDecodedLogs { - transactionHash - } -} -``` - -While this process is running, `viewkit` will keep the local DefraDB instance alive. Press `CTRL` + `c` to stop the DefraDB instance. - -## Deploy to devnet - -Once your view behaves correctly locally, you can deploy it to a shared network. - -1. Gather an RPC URL. -1. Deploy the view to the network: - - ```shell - viewkit view deploy testdeploy --target devnet --rpc http://testnet.shinzo.network:8545/ - ``` - -## More examples - -For progressively more complex View examples (decoding multiple event types, transaction-based views without lenses, materialized vs on-query views, editing and rolling back views), see the [View examples](/build/create-a-view/examples/) page, which includes both the view definitions and the GraphQL queries you run against them. - -For the conceptual overview, see [Views for builders](/build/concepts/views-for-builders/). For the full command list, filter operators, VWL wire format, and deploy internals, see the [Viewkit reference](/reference/components/viewkit/). For a deeper dive on lenses, available modules, and how to chain them, see the [Lens reference](/reference/components/lens/). For troubleshooting and common errors, see [Operations: Troubleshooting](/run/operations/troubleshooting/). - -## Need help - -{{ need_help(client="Viewkit", repo_name="shinzo-view-creator", repo="https://github.com/shinzonetwork/shinzo-view-creator/issues") }} diff --git a/content/build/tutorials/create-your-first-view/index.md b/content/build/tutorials/create-your-first-view/index.md index be223d0..8adde81 100644 --- a/content/build/tutorials/create-your-first-view/index.md +++ b/content/build/tutorials/create-your-first-view/index.md @@ -1,6 +1,418 @@ +++ title = "Create your first View" +aliases = ["/views/quickstart", "/views/install", "/build/create-a-view/"] description = "Tutorial: build, test, and deploy a Shinzo View with Viewkit, from install to querying it on the public testnet." +++ -This page is coming soon. +Viewkit is a CLI tool that helps you initialize, manage, and publish Shinzo views. In this tutorial you will build the `viewkit` executable, assemble a View from its parts, test it locally, and deploy it to the public testnet. If you would rather work from a browser UI instead of the CLI, [Create and deploy Views in Shinzo Studio](/build/how-to/use-shinzo-studio/) covers the same create, deploy, and query flow for Views. + +A View is a versioned bundle with three parts: a query (the raw data shape you ingest), an SDL (the GraphQL schema that models the result), and lenses (WebAssembly transforms that filter, decode, or reshape the data). The View you build here decodes fungible token transfer events from raw logs into readable fields. + +## Prerequisites + +- Git. +- Make. +- Go 1.25 or later. + +## Setup + +1. Make sure the prerequisites are installed properly: + + ```shell + git --version && make --version && go version + ``` + + ```output + git version 2.43.0 + GNU Make 4.3 + [...] + go version go1.25.12 linux/arm64 + ``` + +1. Clone the repository: + + ```shell + git clone https://github.com/shinzonetwork/shinzo-view-creator.git + cd shinzo-view-creator + ``` + +1. Build the Viewkit binary: + + ```shell + make build + ``` + + You should see a `build` directory. + +1. Run Viewkit: + + ```shell + ./build/viewkit --help + ``` + + ```output + Viewkit helps you initialize, manage, and publish Shinzo views through a simple CLI interface. + + Usage: + viewkit [command] + + [...] + ``` + +1. Move the `viewkit` executable somewhere on your PATH (optional): + + ```shell + sudo mv ./build/viewkit /usr/local/bin + ``` + + Now you can run `viewkit` from anywhere. + +## Wasmer runtime + +Viewkit can execute WebAssembly lenses locally to validate and preview them when you run `view test` or deploy locally. + +Under the hood it uses `wasmer-go`, which depends on a native dynamic library (`libwasmer.dylib` on macOS, `libwasmer.so` on Linux). If your system cannot find that library, any command that touches lenses will fail with an error like "image not found" or "library not loaded". + +1. Move back into the shinzo-view-creator repo if you moved out of it: + + ```shell + cd shinzo-view-creator + ``` + +1. Install the Wasmer Go module: + + ```shell + go get github.com/wasmerio/wasmer-go@v1.0.4 + ``` + + ```output + go: downloading github.com/wasmerio/wasmer-go v1.0.4 + go: added github.com/wasmerio/wasmer-go v1.0.4 + ``` + + This makes `wasmer-go` and its packaged native libraries available in your `GOPATH`. + +### Environment variables + +You need to set three environment variables: + +- `WASMER_ROOT`: points to the directory where the Wasmer dynamic library lives. +- `WASMER_LIB_PATH`: used by `wasmer-go` to find the dynamic library. +- `DYLD_LIBRARY_PATH` (macOS) or `LD_LIBRARY_PATH` (Linux): the dynamic loader search path. Prepend `WASMER_ROOT` so the loader finds the library when `viewkit` starts. + +1. Append these lines to your shell's RC file. + + macOS: + + ```shell + echo 'export WASMER_ROOT="$(go env GOPATH)/pkg/mod/github.com/wasmerio/wasmer-go@v1.0.4/wasmer/packaged/lib/darwin-aarch64"' >> ~/.zshrc + echo 'export WASMER_LIB_PATH="$WASMER_ROOT"' >> ~/.zshrc + echo 'export DYLD_LIBRARY_PATH="$WASMER_ROOT:$DYLD_LIBRARY_PATH"' >> ~/.zshrc + ``` + + Linux: + + ```shell + echo 'export WASMER_ROOT="$(go env GOPATH)/pkg/mod/github.com/wasmerio/wasmer-go@v1.0.4/wasmer/packaged/lib/linux-amd64"' >> ~/.zshrc + echo 'export WASMER_LIB_PATH="$WASMER_ROOT"' >> ~/.zshrc + echo 'export LD_LIBRARY_PATH="$WASMER_ROOT:$LD_LIBRARY_PATH"' >> ~/.zshrc + ``` + +1. Reload your shell configuration: + + ```shell + source ~/.zshrc + ``` + +1. Verify the variables are set: + + ```shell + echo "$WASMER_ROOT" + ls "$WASMER_ROOT" + ``` + + ```output + /home/user/go/pkg/mod/github.com/wasmerio/wasmer-go@v1.0.4/wasmer/packaged/lib/linux-amd64 + dummy.go libwasmer.so + ``` + +{% admonition(type="warning") %} +If `libwasmer.dylib` or `libwasmer.so` is missing, re-run the `go get` step and check that `go env GOPATH` returns a valid path. The local `view test` and `view deploy --target local` commands spawn a DefraDB binary that also needs `LD_LIBRARY_PATH` (Linux) or `DYLD_LIBRARY_PATH` (macOS) to be set, so keep these variables set in any shell where you run Viewkit. +{% end %} + +## Create a View + +1. Initialize the View bundle: + + ```shell + viewkit view init testdeploy + ``` + + This creates a new View bundle called `testdeploy` on disk and registers internal metadata for queries, SDL, lenses, and versions. + +1. Add a query defining the raw data shape to ingest. Here it is raw event logs: + + ```shell + viewkit view add query \ + "Log {address topics data transactionHash blockNumber}" \ + --name testdeploy + ``` + + This tells Viewkit that `testdeploy` will ingest `Log` objects with the specified fields. + +1. Add an SDL describing how the data is modeled and exposed: + + ```shell + viewkit view add sdl \ + "type FilteredAndDecodedLogs @materialized(if: false) {transactionHash: String}" \ + --name testdeploy + ``` + + {% admonition(type="note") %} +- `@materialized(if: false)`: treat this as a virtual type, computed at query time rather than stored. +- `transactionHash: String`: a minimal example field; real Views define more fields. + {% end %} + +1. Attach a WebAssembly lens that decodes event logs using an ABI: + + ```shell + viewkit view add lens \ + --args '{"abi":"[{\"type\":\"event\",\"name\":\"Transfer\",\"inputs\":[{\"type\":\"address\",\"name\":\"from\",\"indexed\":true},{\"type\":\"address\",\"name\":\"to\",\"indexed\":true},{\"type\":\"uint256\",\"name\":\"value\",\"indexed\":false}]}]"}' \ + --label "decode" \ + --url "https://raw.githubusercontent.com/shinzonetwork/wasm-bucket/main/bucket/decode_log/decode_log.wasm" \ + --name testdeploy + ``` + + These are the flags in play: + + - `--args`: JSON passed to the lens. Here it is an ABI definition for a `Transfer` event with `from`, `to`, and `value` fields. + - `--label "decode"`: a human-readable label for the lens. + - `--url`: remote URL of the `.wasm` binary. + - `--name testdeploy`: attaches this lens to the `testdeploy` View. + +1. Inspect the bundle once everything is attached: + + ```shell + viewkit view inspect testdeploy + ``` + + ```output + 📄 View: testdeploy + 🔍 Query: + Log {address topics data transactionHash blockNumber} + + 📐 SDL: + type FilteredAndDecodedLogs @materialized(if: false) {transactionHash: String} + + 🔧 Lenses: + - decode (assets/decode.wasm) + Arguments: + abi: [{"type":"event","name":"Transfer","inputs":[{"type":"address","name":"from","indexed":true},{"type":"address","name":"to","indexed":true},{"type":"uint256","name":"value","indexed":false}]}] + + 🗂 Metadata: + - Version: 3 + - Total: 3 + - Created At: 2026-09-03 11:35:10 +0000 UTC + - Updated At: 2026-09-03 11:35:28 +0000 UTC + ``` + + Every `view add` command also prints this same summary, so running `inspect` once after all the parts are attached is enough. + +{% admonition(type="tip") %} +If you see `libwasmer.dylib` or "image not found" errors, revisit the Wasmer setup. +{% end %} + +## Test the View + +1. Before deploying, validate that the View builds and compiles successfully: + + ```shell + viewkit view test testdeploy + ``` + + ```output + 🔍 Loading view... + ⚙️ Ensuring DefraDB binary... + 📁 Creating temporary root directory... + 🚀 Starting DefraDB... + ⏳ Waiting for DefraDB to boot... + ✅ DefraDB booted + 📦 Applying schema... + ✅ Schema applied + 📨 Inserting test data... + ⏳ Data Inserting... + ✅ Data Inserted Successfully + ✅ Data inserted + 🧠 Applying view... + ✅ View applied + 🔎 Extracting collection name... + ♻️ Refreshing view... + ✅ View refreshed + ✅ Test flow completed successfully. Shutting down... + ✅ DefraDB stopped. + ``` + + This spins up a temporary local DefraDB instance, applies the schema, runs the lens, and checks that everything compiles. If it passes, the View is ready to deploy. + +## Create a deployment wallet + +Deploying to the network registers the View on chain, so you need a wallet to sign that transaction. + +1. Generate a wallet: + + ```shell + viewkit wallet generate + ``` + + ```output + ✅ Wallet generated + Mnemonic: document grass code lawn erosion climb people sunset three blame balcony story script hip soup lesson resemble above quiz acid dust salmon plane + Address: 0x2e4150993E841b38f4780BC158A7dA0d62E22ec9 + ``` + +1. Treat this wallet like any other secret: + + - Do not commit it to Git. + - Do not paste the mnemonic in public places. + - Store it securely. + +## Fund the wallet + +Registration is an on-chain transaction, so the wallet needs tokens to pay the transaction fee. + +1. Open the [Shinzo faucet](https://faucet.shinzo.network/) in your browser. +1. Paste the address from `viewkit wallet generate` into the input. +1. Click **Get 0.001 $SHN**. + +The faucet shows the transaction hash once the tokens are sent. You only need to do this once per wallet. + +## Deploy locally + +The recommended flow is to deploy locally first, verify the View in the DefraDB Playground, then deploy to the network. + +1. Deploy locally: + + ```shell + viewkit view deploy testdeploy --target local + ``` + + ```output + 🚀 DefraDB is running on port 9181 + ⏳ Waiting for DefraDB to boot up... + ✅ DefraDB booted up + ⏳ Applying Schemas ... + ✅ Schema Applied + ⏳ Data Inserting... + ✅ Data Inserted Successfully + ✅ Applying View ... + ✅ View Successfully Applied + 🧪 Visit the DefraDB GraphQL Playground at http://127.0.0.1:9181/ + 📦 Press Ctrl+C to stop... + ``` + + Here's what happens: + + 1. A local DefraDB instance starts (the port is in the logs). + 1. The schema for the View is applied. + 1. Test data is inserted. + 1. The View is applied. + 1. A DefraDB GraphQL Playground URL is printed. + +### Use the DefraDB GraphQL Playground + +This step is optional, but it is a good way to check the View before deploying it anywhere. + +1. Open the displayed URL in your browser, usually [127.0.0.1:9181](http://127.0.0.1:9181/). +1. You should see a GraphQL Playground where you can: + - Inspect the schema (for example, the `FilteredAndDecodedLogs` type). + - Run test queries against the local View. + - Confirm the lens decodes logs as expected. + + For example: + + ```graphql + { + FilteredAndDecodedLogs { + transactionHash + } + } + ``` + +While this process is running, `viewkit` keeps the local DefraDB instance alive. Press `CTRL` + `c` to stop the DefraDB instance. + +## Deploy to the public testnet + +Once the View behaves correctly locally, deploy it to the public testnet. + +1. Check that the wallet has a balance, and fund it from the [faucet](https://faucet.shinzo.network/) if needed. + +1. Deploy the View to the network: + + ```shell + viewkit view deploy testdeploy --target devnet --rpc http://testnet.shinzo.network:8545/ + ``` + +{% admonition(type="note") %} +The `--target devnet` flag name is historical. In the current Viewkit it is the right target for the public testnet (`--rpc http://testnet.shinzo.network:8545/`). The CLI's `mainnet` target is not supported yet, so `devnet` is the only network target that works. +{% end %} + +The deploy command rebuilds and re-tests the View, sends a `register(bytes)` transaction to the View Registry precompile, and prints the result: + +```output +🔧 Building and testing view before deployment... +⏳ View built and tested successfully. Deploying... +✅ View deployment successful! +---------------------------------------- +🔑 View ID: FilteredAndDecodedLogs_0x016c19db7f2cf5aa86d34b5779f58de913f92d12ec9ee3642587124ce712c123 +🔑 View Key: 0x016c19db7f2cf5aa86d34b5779f58de913f92d12ec9ee3642587124ce712c123 +📦 Transaction Hash: 0x7be8a3e99299ed017896ae76fb2811208380a459e36d49a0b4a7ac047747a2df +Wire bytes (tx data):96660 +---------------------------------------- +``` + +Registration completes asynchronously, so give it around 20 seconds before the View shows up on chain. + +## Query your View + +After registration, the View is on chain and any Host that picks it up can serve it. + +1. Confirm your View is registered: + + ```shell + curl "http://testnet.shinzo.network:1317/shinzonetwork/view/v1/views?include_data=false" \ + | jq -r '.views[] | select(.name | contains("FilteredAndDecodedLogs")) | .name, .address' + ``` + + ```output + FilteredAndDecodedLogs + 0xa1226B03c54789e9Bf8876ac956aBbD1bDf5B654 + ``` + +1. Query the View against a public Host: + + ```shell + HOST=$(curl -s "http://testnet.shinzo.network:1317/shinzonetwork/host/v1/hosts" \ + | jq -r '.hosts[0].endpoint_address') + + curl -X POST "$HOST" \ + -H 'Content-Type: application/json' \ + -d '{"query":"{ FilteredAndDecodedLogs(limit: 10) { transactionHash } }"}' + ``` + +Until a Host picks up your View, a query against it returns a schema error: + +```output +{"errors":[{"message":"Cannot query field \"FilteredAndDecodedLogs\" on type \"Query\"."}],"data":null} +``` + +Hosts subscribe to a View and serve it once it has a pool; a brand-new tutorial View usually has none yet. Signed querying is covered in [Query your first View](/build/tutorials/query-your-first-view/), and [Find Views and Hosts](/build/how-to/find-views-and-hosts/) shows how to locate Host endpoints and pools. Browse your View in [Shinzo Studio](https://studio.shinzo.network/) or the [Explorer](https://explorer.shinzo.network/). + +## More examples + +For progressively more complex View examples (decoding multiple event types, transaction-based Views without lenses, materialized versus on-query Views, editing and rolling back Views), see [View recipes](/build/how-to/view-recipes/), which includes both the View definitions and the GraphQL queries you run against them. + +For the conceptual overview, see [Views for builders](/build/explanation/views-for-builders/). For the full command list, filter operators, VWL wire format, and deploy internals, see the [Viewkit reference](/reference/components/viewkit/). For a deeper dive on lenses, available modules, and how to chain them, see the [Lens reference](/reference/components/lens/). For troubleshooting and common errors, see [Operations: Troubleshooting](/run/operations/troubleshooting/). + +## Need help + +{{ need_help(client="Viewkit", repo_name="shinzo-view-creator", repo="https://github.com/shinzonetwork/shinzo-view-creator/issues") }} diff --git a/content/reference/components/lens/index.md b/content/reference/components/lens/index.md index e865b8a..a719d58 100644 --- a/content/reference/components/lens/index.md +++ b/content/reference/components/lens/index.md @@ -204,7 +204,7 @@ This spins up a temporary local DefraDB instance, applies your schema, loads the viewkit view deploy my-view --target local ``` -See the [Quick Start](/build/create-a-view/) for Wasmer runtime setup if you encounter `libwasmer` errors during local testing. +See the [Create your first View](/build/tutorials/create-your-first-view/) tutorial for Wasmer runtime setup if you encounter `libwasmer` errors during local testing. ## Authoring new lenses diff --git a/content/reference/components/viewkit/index.md b/content/reference/components/viewkit/index.md index eeea336..ec90e53 100644 --- a/content/reference/components/viewkit/index.md +++ b/content/reference/components/viewkit/index.md @@ -6,7 +6,7 @@ mermaid = true Viewkit is the local CLI tool for creating, testing, and deploying Shinzo Views. It packages a view into a binary bundle (VWL) and submits a deploy transaction to ShinzoHub. It does not process, store, or serve data. -This page is the technical reference for Viewkit: the full command list, filter operators, the deploy pipeline, the VWL wire format, view ID computation, and the on-disk layout of the source repo. For a hands-on walkthrough of building and deploying your first view, see the [Create a View](/build/create-a-view/) quickstart. For the conceptual overview of what Viewkit is and where it sits in the stack, see [Views for builders](/build/concepts/views-for-builders/). +This page is the technical reference for Viewkit: the full command list, filter operators, the deploy pipeline, the VWL wire format, view ID computation, and the on-disk layout of the source repo. For a hands-on walkthrough of building and deploying your first view, see the [Create your first View](/build/tutorials/create-your-first-view/) tutorial. For the conceptual overview of what Viewkit is and where it sits in the stack, see [Views for builders](/build/concepts/views-for-builders/). ## Command reference diff --git a/content/run/operations/troubleshooting/index.md b/content/run/operations/troubleshooting/index.md index 5acc828..cef3a9f 100644 --- a/content/run/operations/troubleshooting/index.md +++ b/content/run/operations/troubleshooting/index.md @@ -208,7 +208,7 @@ A healthy Host returns `healthy`. A connection refused or timeout means `8080` s Viewkit uses the Wasmer runtime to execute WASM lenses locally. If the native library can't be found, any command that touches lenses will fail. -Fix: set the `WASMER_ROOT`, `WASMER_LIB_PATH`, and `DYLD_LIBRARY_PATH` (macOS) or `LD_LIBRARY_PATH` (Linux) environment variables. See [Create a View](/build/create-a-view/#wasmer-runtime) for full instructions. +Fix: set the `WASMER_ROOT`, `WASMER_LIB_PATH`, and `DYLD_LIBRARY_PATH` (macOS) or `LD_LIBRARY_PATH` (Linux) environment variables. See [Create your first View](/build/tutorials/create-your-first-view/#wasmer-runtime) for full instructions. Quick check: diff --git a/content/run/run-a-host/install/index.md b/content/run/run-a-host/install/index.md index a35684a..2cf438b 100644 --- a/content/run/run-a-host/install/index.md +++ b/content/run/run-a-host/install/index.md @@ -6,7 +6,7 @@ aliases = ["/hosts/install"] A Host client pulls primitive blockchain data from Generator clients, runs Lens WASM transforms, and serves the resulting Views to subscriber nodes over an embedded DefraDB instance. This page is for operators who want to **run** a Host client. {% admonition(type="info") %} -**Only want to query Shinzo data?** You don't need to run your own Host client. Connect to a public Host client instead. See [Querying Views](/build/create-a-view/). Running your own Host client is for serving data to the network, not for reading it. +**Only want to query Shinzo data?** You don't need to run your own Host client. Connect to a public Host client instead. See [Query your first View](/build/tutorials/query-your-first-view/). Running your own Host client is for serving data to the network, not for reading it. {% end %} ## Prerequisites @@ -95,7 +95,7 @@ Pull the image and start it with a single `docker run`. You supply two values: a docker logs shinzo-host | grep -i peer ``` - A healthy connection adds the Generator client's peer and keeps it. You should not see that peer stuck in a loop of `dial backoff` / `all dials failed`. Once it's syncing, query real data from the Playground or the API. A public Generator client to point at is coming; this page will link a known-good endpoint and a ready-to-run query once it's live. See [query examples](/build/create-a-view/). + A healthy connection adds the Generator client's peer and keeps it. You should not see that peer stuck in a loop of `dial backoff` / `all dials failed`. Once it's syncing, query real data from the Playground or the API. A public Generator client to point at is coming; this page will link a known-good endpoint and a ready-to-run query once it's live. See [query examples](/build/how-to/query-data/). 1. To serve data to the network, register your Host client with ShinzoHub. See [Register a Host](/run/run-a-host/register/). diff --git a/content/understand/core-concepts/pools/index.md b/content/understand/core-concepts/pools/index.md index d80d7b3..3f71673 100644 --- a/content/understand/core-concepts/pools/index.md +++ b/content/understand/core-concepts/pools/index.md @@ -33,7 +33,7 @@ A pool closes that gap. Registering demand for a View, and backing it with a bon A pool is created the first time someone registers demand for a View through the Pool Registry on ShinzoHub. The demand carries a bond in ushinzo (SHNZ's base unit), which has to be more than zero and is escrowed as a sign of real intent. If no pool exists yet for that View, one is created; later demand for the same View joins the existing pool rather than making a new one. -A pool is identified by two things: the View it serves, and a small config. Today that config is a single field, the pool's `windowSize`. Changing it produces a different pool, so one View can have several pools under different configs. The View has to be registered first; you can't register demand against a View that doesn't exist yet. See [Create a View](/build/create-a-view/) for how to get that far. +A pool is identified by two things: the View it serves, and a small config. Today that config is a single field, the pool's `windowSize`. Changing it produces a different pool, so one View can have several pools under different configs. The View has to be registered first; you can't register demand against a View that doesn't exist yet. See [Create your first View](/build/tutorials/create-your-first-view/) for how to get that far. ## Two faces of a pool diff --git a/static/_redirects b/static/_redirects index aee9ed1..6608272 100644 --- a/static/_redirects +++ b/static/_redirects @@ -1,6 +1,6 @@ # Legacy path redirects to the current information architecture. Paths handled by Zola aliases on content pages (/, /introduction, /understand) are not duplicated here. -/guides /build/create-a-view/ +/guides /build/tutorials/create-your-first-view/ /host /run/run-a-host/ /host/* /run/run-a-host/:splat /hosts /run/run-a-host/ @@ -11,9 +11,9 @@ /indexer/* /run/run-a-generator/:splat /indexers /run/run-a-generator/ /indexers/* /run/run-a-generator/:splat -/views /build/create-a-view/ -/view-creator /build/create-a-view/ -/view-creator/* /build/create-a-view/:splat +/views /build/tutorials/create-your-first-view/ +/view-creator /build/tutorials/create-your-first-view/ +/view-creator/* /build/tutorials/create-your-first-view/ /reference /reference/architecture/ /reference/components /reference/components/ @@ -42,5 +42,5 @@ /docs/indexer/quickstart/* /run/get-started/ 301 /docs/view-creator/overview /build/concepts/views-for-builders/ 301 /docs/view-creator/overview/* /build/concepts/views-for-builders/ 301 -/docs/view-creator/quickstart /build/create-a-view/ 301 -/docs/view-creator/quickstart/* /build/create-a-view/ 301 +/docs/view-creator/quickstart /build/tutorials/create-your-first-view/ 301 +/docs/view-creator/quickstart/* /build/tutorials/create-your-first-view/ 301 diff --git a/templates/macros/footer.html b/templates/macros/footer.html index f5caadf..6736443 100644 --- a/templates/macros/footer.html +++ b/templates/macros/footer.html @@ -16,7 +16,7 @@ From 1f97f433f47a21c185c1b9c2e57f31e1c42b259f Mon Sep 17 00:00:00 2001 From: johnnymatthews <9611008+johnnymatthews@users.noreply.github.com> Date: Sat, 5 Sep 2026 12:43:07 +0100 Subject: [PATCH 09/11] Uses mermaid diagrams instead. --- .../tutorials/spinup-a-local-devnet/index.md | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/content/build/tutorials/spinup-a-local-devnet/index.md b/content/build/tutorials/spinup-a-local-devnet/index.md index 53316fb..240a3f4 100644 --- a/content/build/tutorials/spinup-a-local-devnet/index.md +++ b/content/build/tutorials/spinup-a-local-devnet/index.md @@ -1,6 +1,8 @@ +++ title = "Spinup a local devnet" description = "Tutorial: start a devnet locally on your machine to test basic Shinzo and blockchain functionality." +[extra] +mermaid = true +++ This tutorial walks through spinning up a local Ethereum development network using Geth, deploying an ERC-20 smart contract to it, and connecting the Shinzo Generator client and Host so blockchain data is generated into DefraDB and queryable via GraphQL. @@ -9,17 +11,13 @@ This tutorial walks through spinning up a local Ethereum development network usi Here's the basic flow for what we're about to build: -```plaintext -Geth (--dev) → mines blocks on demand - ↓ -Shinzo Generator client → watches for new blocks via HTTP/WS RPC - ↓ -DefraDB (embedded in Generator client) → stores generated block/transaction/log data - ↓ (P2P passive replication) -DefraDB (embedded in Host) → receives replicated data - ↓ -GraphQL API (port 9182) → query the replicated data -``` +{% mermaid() %} +flowchart LR + Geth["Geth (--dev)
mines blocks on demand"] --> Gen["Shinzo Generator client
watches for new blocks via HTTP/WS RPC"] + Gen --> GenDB["DefraDB (embedded in Generator client)
stores generated block/transaction/log data"] + GenDB -- "P2P passive replication" --> HostDB["DefraDB (embedded in Host)
receives replicated data"] + HostDB --> GraphQL["GraphQL API (port 9182)
query the replicated data"] +{% end %} ### Prerequisites From 36af9928856a199fa87f3960bdd5438c9be04471 Mon Sep 17 00:00:00 2001 From: johnnymatthews <9611008+johnnymatthews@users.noreply.github.com> Date: Sat, 5 Sep 2026 12:45:25 +0100 Subject: [PATCH 10/11] Removes "Module" from glossary. Pretty common term. Not really needed. --- data/glossary.json | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/data/glossary.json b/data/glossary.json index 6406aff..9af69b8 100644 --- a/data/glossary.json +++ b/data/glossary.json @@ -134,8 +134,7 @@ "relatedTerms": [ "ShinzoHub", "SourceHub", - "CometBFT", - "Module" + "CometBFT" ] }, { @@ -347,7 +346,6 @@ "definition": "In Cosmos SDK, a Go object that owns a module's state and provides the only sanctioned way to read or write it. Precompile code on ShinzoHub calls keepers directly — for example, to send an ICA packet — without going through Solidity bytecode.", "relatedTerms": [ "Cosmos SDK", - "Module", "Precompile" ] }, @@ -431,15 +429,6 @@ "Assertion" ] }, - { - "term": "Module", - "definition": "A Cosmos SDK building block that owns a slice of chain state, handles messages, and emits events. ShinzoHub adds five custom modules (`x/admin`, `x/sourcehub`, `x/host`, `x/generator`, `x/view`) on top of the standard Cosmos set.", - "relatedTerms": [ - "Cosmos SDK", - "ShinzoHub", - "Keeper" - ] - }, { "term": "Network Gateway", "definition": "A coordination service that routes user GraphQL queries to the right host. It never touches the underlying data.", From b978345500b37c54a7c2f2a2e46e5bb32b109dec Mon Sep 17 00:00:00 2001 From: johnnymatthews <9611008+johnnymatthews@users.noreply.github.com> Date: Sat, 5 Sep 2026 12:57:40 +0100 Subject: [PATCH 11/11] Fixes tooltip logic. --- qa-tooltips.py | 42 +++++++++++++++++++-------- static/js/glossary-tooltips.js | 24 ++++++++++----- templates/partials/glossary-data.html | 7 +++++ 3 files changed, 54 insertions(+), 19 deletions(-) diff --git a/qa-tooltips.py b/qa-tooltips.py index eb2ceaa..b6c0005 100644 --- a/qa-tooltips.py +++ b/qa-tooltips.py @@ -14,10 +14,13 @@ DEFINED = {t["term"].lower(): t["definition"] for t in GLOSSARY if t.get("definition")} SKIP_TAGS = {"code", "pre", "a", "h1", "h2", "h3", "h4", "h5", "h6", "script", "style"} +SKIP_CLASSES = {"mermaid", "breadcrumbs"} -# Longest-first alternation, whole-word boundaries — mirrors the JS regex. -NAMES = sorted(DEFINED, key=len, reverse=True) -RE = re.compile(r"\b(" + "|".join(re.escape(n) for n in NAMES) + r")\b", re.I) +# Canonical spellings, longest-first alternation, whole-word boundaries, and +# no match touching a hyphen — mirrors the JS regex. Case-sensitive: "View" +# is a term, "view" is not. +NAMES = sorted((t["term"] for t in GLOSSARY if t.get("definition")), key=len, reverse=True) +RE = re.compile(r"(? etc.) don't leak skip depth + self.hidden_chunks = [] # text inside skipped subtrees self.injected_terms = None def handle_starttag(self, tag, attrs): cls = dict(attrs).get("class", "") + opens_skip = bool(self.in_article and (tag in SKIP_TAGS or cls and any(c in SKIP_CLASSES for c in cls.split()))) + self.stack.append((tag, opens_skip)) if tag == "article" and "markdown" in cls: self.in_article += 1 - elif self.in_article and (tag in SKIP_TAGS or "mermaid" in cls): + elif opens_skip: self.skip_depth += 1 elif tag == "script" and not dict(attrs).get("src"): self._capture = True @@ -52,8 +58,12 @@ def handle_starttag(self, tag, attrs): def handle_endtag(self, tag): if tag == "article" and self.in_article: self.in_article -= 1 - elif self.in_article and self.skip_depth: - self.skip_depth -= 1 + while self.stack: + opened = self.stack.pop() + if opened[1]: + self.skip_depth -= 1 + if opened[0] == tag: + break def handle_data(self, data): if self.in_article: @@ -80,7 +90,7 @@ def false_positives(page): "reference/architecture", "understand/core-concepts/views", "reference/glossary", - "build/query-data", + "build/how-to/query-data", "run/run-a-generator/install", ] @@ -119,14 +129,22 @@ def false_positives(page): # Whole-word guarantee: a term that is a prefix of a longer word used in the # docs ("View" vs "ViewKit", "Host" vs "HostRegistry") must not match inside it. -sample_false = ["review", "hostname", "LogEntry", "poolside", "Bonded", "Preview"] +sample_false = ["review", "hostname", "LogEntry", "poolside", "Bonded", "Preview", + "ViewKit", "view", "shinzo-view-creator", "view-creator"] bad = [w for w in sample_false if RE.search(w)] -check("regex: no match inside longer words (" + ", ".join(sample_false) + ")", not bad, +check("regex: no match inside longer or hyphenated words (" + ", ".join(sample_false) + ")", not bad, "matched: " + ", ".join(bad)) +# Case sensitivity: "View" is a term, lowercase "view" is not. +check("regex: case-sensitive (\"View\" matches, \"view\" does not)", + RE.search("View") is not None and not RE.search("view")) # Multi-word terms match as phrases. -multi = [t for t in DEFINED if " " in t] +multi = [t["term"] for t in GLOSSARY if " " in t["term"] and t.get("definition")] check("regex: multi-word terms match (" + str(len(multi)) + " phrases)", all(RE.search(t) for t in multi)) +# Hyphenated terms (EIP-2930, MEV-boost, ...) still match as whole tokens. +hyph = [t["term"] for t in GLOSSARY if "-" in t["term"] and t.get("definition")] +check("regex: hyphenated terms match (" + str(len(hyph)) + " terms)", + all(RE.search(t) for t in hyph)) print("\n" + (str(failures) + " FAILURES" if failures else "ALL CHECKS PASSED")) sys.exit(1 if failures else 0) diff --git a/static/js/glossary-tooltips.js b/static/js/glossary-tooltips.js index 873d7a9..335d803 100644 --- a/static/js/glossary-tooltips.js +++ b/static/js/glossary-tooltips.js @@ -10,7 +10,8 @@ // so it won't. (function () { var terms = window.glossaryTerms; - if (!terms) return; + var names = window.glossaryTermNames; + if (!terms || !names) return; var article = document.querySelector("article.markdown"); if (!article) return; @@ -18,23 +19,29 @@ // One alternation, longest first, so multi-word phrases ("Merkle Tree") win // over their prefixes and every occurrence on the page gets tagged, not just // the first. Whole-word boundaries on both ends keep "View" from matching - // inside "ViewKit" or "review". - var names = Object.keys(terms).sort(function (a, b) { return b.length - a.length; }); + // inside "ViewKit" or "review", and the tagging loop below also rejects + // matches with a hyphen glued to either end, so "view" stays plain inside + // "shinzo-view-creator" while hyphenated terms like "EIP-2930" still match + // as whole tokens. Matching is case-sensitive — "View" is a term, "view" is + // not — so names keep their canonical spelling here while the term keys in + // window.glossaryTerms stay lowercase for lookups. + names = names.slice().sort(function (a, b) { return b.length - a.length; }); if (!names.length) return; var escaped = names.map(function (n) { return n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }); - var re = new RegExp("\\b(" + escaped.join("|") + ")\\b", "gi"); + var re = new RegExp("\\b(" + escaped.join("|") + ")\\b", "g"); // Subtrees where a tooltip would get in the way: code blocks, inline code, - // existing links, headings (also keeps the page TOC clean), and Mermaid - // diagrams (tagging a node label corrupts the diagram source text). + // existing links, headings (also keeps the page TOC clean), Mermaid + // diagrams (tagging a node label corrupts the diagram source text), and the + // breadcrumbs (navigation chrome, not page prose). var SKIP = { CODE: 1, PRE: 1, A: 1, H1: 1, H2: 1, H3: 1, H4: 1, H5: 1, H6: 1, SCRIPT: 1, STYLE: 1 }; function insideSkipped(node) { for (var n = node.parentNode; n && n !== article; n = n.parentNode) { if (SKIP[n.nodeName]) return true; - if (n.classList && n.classList.contains("mermaid")) return true; + if (n.classList && (n.classList.contains("mermaid") || n.classList.contains("breadcrumbs"))) return true; } return false; } @@ -55,6 +62,9 @@ var m; re.lastIndex = 0; while ((m = re.exec(text))) { + // A term must be the whole hyphen-delimited token: skip matches with a + // hyphen on either side ("view" inside "shinzo-view-creator"). + if (text.charAt(m.index - 1) === "-" || text.charAt(m.index + m[0].length) === "-") continue; if (m.index > last) frag.appendChild(document.createTextNode(text.slice(last, m.index))); var span = document.createElement("span"); span.className = "glossary-term"; diff --git a/templates/partials/glossary-data.html b/templates/partials/glossary-data.html index bcf34a7..0df3adf 100644 --- a/templates/partials/glossary-data.html +++ b/templates/partials/glossary-data.html @@ -10,4 +10,11 @@ {{ t.term | lower | json_encode | safe }}: {{ t.definition | markdown(inline=true) | json_encode | safe }}{% if not loop.last %},{% endif %} {%- endfor %} }; +{# Canonical spellings for the tagger's case-sensitive regex; the map keys + above stay lowercase so lookups work whatever the text's capitalisation. #} +window.glossaryTermNames = [ +{%- for t in defined %} + {{ t.term | json_encode | safe }}{% if not loop.last %},{% endif %} +{%- endfor %} +];