Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion .github/workflows/example-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ on:
types: [opened, synchronize, reopened]
paths:
- "examples/status-message-dapp/**"
- "examples/perch-status-onchain/**"
- ".github/workflows/example-preview.yml"
# The example exercises the wallet's connect / delegation-handover /
# session-signing flows, so a wallet or SDK PR needs the example preview
Expand Down Expand Up @@ -60,6 +61,7 @@ jobs:
npm run build -w @nidohq/passkey-sdk
npm run build -w @nidohq/stellar-wallets-kit-module
npm run build -w status_message
npm run build -w @nidohq/testkit

- name: Build the example (apex base)
run: npx vite build
Expand All @@ -69,6 +71,16 @@ jobs:
run: echo "/* /index.html 200" > dist/_redirects
working-directory: examples/status-message-dapp

# Extra examples nested under their own subpath in the same deploy.
# Relative base (--base=./) so each loads correctly from its subpath.
- name: Build the perch tour (nested)
run: npm run build:preview
working-directory: examples/perch-status-onchain

- name: Nest the tour under the deploy
run: |
cp -r examples/perch-status-onchain/dist examples/status-message-dapp/dist/perch-status-onchain

# Deploy to a branch alias of the mysoroban project. The custom-domain
# wildcard (*.mysoroban.xyz) only serves the project's PRODUCTION branch,
# so for the example we use wrangler's branch-alias *.pages.dev URL — the
Expand Down Expand Up @@ -101,7 +113,11 @@ jobs:
const previewUrl = process.env.PREVIEW_URL
|| `https://example-pr-${prNum}.mysoroban.pages.dev`;
const liveUrl = `https://${context.repo.owner}.github.io/${context.repo.repo}/`;
const body = `**Example dApp preview deployed!**\n\n${previewUrl}\n\nThe \`status-message\` example (testnet), wallet = THIS PR's preview (https://${prNum}.nido.fyi). The live home is ${liveUrl} once merged.`;
const base = previewUrl.replace(/\/?$/, '/');
const body = `**Example dApp preview deployed!**\n\n`
+ `- \`status-message\` — ${previewUrl}\n`
+ `- \`perch-status-onchain\` (guided tour) — ${base}perch-status-onchain/\n\n`
+ `status-message runs on testnet, wallet = THIS PR's preview (https://${prNum}.nido.fyi); the perch tour drives perch on real testnet. The live home is ${liveUrl} once merged.`;

const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
Expand Down
42 changes: 29 additions & 13 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
name: Deploy Example to GitHub Pages
name: Deploy examples to GitHub Pages

# Deploys examples/status-message-dapp/ as a live testnet demo at the GitHub
# Pages project URL for this repository. Independent of the Cloudflare deploy of
# the main Nido frontend (see deploy.yml / preview.yml).
# Deploys the repo's example dApps as a live testnet demo at the GitHub Pages
# project URL. status-message-dapp is the home (project root); each additional
# example is nested under its own subpath. To add an example: build it into a
# subfolder of the uploaded site (see the "extra examples" step). Independent of
# the Cloudflare deploy of the main Nido frontend (deploy.yml / preview.yml).
#
# /<repo>/ → status-message-dapp
# /<repo>/perch-status-onchain/ → perch guided tour (enforced on live testnet)

on:
push:
branches: [main]
paths:
- "examples/status-message-dapp/**"
- "examples/perch-status-onchain/**"
- "packages/**"
- ".github/workflows/pages.yml"
workflow_dispatch:

Expand All @@ -17,14 +24,11 @@ permissions:
pages: write
id-token: write

# Allow one concurrent deployment; don't cancel an in-progress publish.
concurrency:
group: pages
cancel-in-progress: false

# Build-time config for the live demo. All public (no secrets). The committed
# client is bound to the testnet contract id; these point the frontend at
# testnet and at the production Nido passkey wallet.
# Build-time config for the status-message live demo. All public (no secrets).
env:
PAGES_BASE_PATH: "/${{ github.event.repository.name }}/"
PUBLIC_STELLAR_NETWORK: TESTNET
Expand All @@ -47,24 +51,36 @@ jobs:
- name: Install dependencies
run: npm ci

# The example imports these workspace packages, which publish built dist/.
# The status_message client is committed as source and built here too.
# Workspace packages the examples import (each publishes a built dist/).
- name: Build workspace dependencies
run: |
npm run build -w @nidohq/passkey-sdk
npm run build -w @nidohq/stellar-wallets-kit-module
npm run build -w status_message
npm run build -w @nidohq/testkit

- name: Build the example (GitHub Pages base)
# --- home: status-message-dapp at the project root ---
- name: Build status-message-dapp (GitHub Pages base)
run: npm run build:pages
working-directory: examples/status-message-dapp

# GitHub Pages serves 404.html for unmatched paths; making it the SPA
# shell lets client-side routes under the project path survive a direct reload.
# GitHub Pages serves 404.html for unmatched paths; making it the SPA shell
# lets status-message's client-side routes survive a direct reload.
- name: SPA fallback
run: cp dist/index.html dist/404.html
working-directory: examples/status-message-dapp

# --- extra examples: each nested under its own subpath ---
# Relative base (--base=./) so the bundle loads correctly from the subpath.
- name: Build the perch tour (nested)
run: npm run build:preview
working-directory: examples/perch-status-onchain

- name: Nest the tour under the site
run: |
cp -r examples/perch-status-onchain/dist \
examples/status-message-dapp/dist/perch-status-onchain

- uses: actions/configure-pages@v5

- uses: actions/upload-pages-artifact@v3
Expand Down
4 changes: 4 additions & 0 deletions examples/perch-status-onchain/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
test-results/
dist/
node_modules/
thresholdProof.json
122 changes: 122 additions & 0 deletions examples/perch-status-onchain/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Scope a key with perch — a Nido guided tour

A guided, six-act tour that takes one job — **"give your CI pipeline a key that
ships releases but can never touch admin or move funds — and prove it"** — from a
raw keypair all the way to a policy the chain enforces, then adds a **multi-sig
quorum**. It teaches, in order, what a smart account is, the OpenZeppelin model,
what Nido adds, **how perch makes authorization easier and safer**, and **how a
rule can require M-of-N signatures** — each ending in a live testnet demonstration.

Styled in Nido's own "Warm Nest" design language.

## The six acts

1. **One key, total power** — a Stellar G-address is all-or-nothing; handing it
to CI hands over the treasury. *The problem.*
2. **The account becomes a program** — a smart account (C-address) runs your
`__check_auth`; authorization is now code.
3. **OZ gives you the vocabulary** — `Signer` + `ContextRule` + **policies** —
but policies are contracts you write, deploy, and audit (with an INV-2
footgun that can brick your admin).
4. **Nido makes it human** — passkeys, a factory, recovery; connect a real
account and see its **signers across verifiers** — secp256r1 (live), a
**post-quantum ML-DSA-65** key, and a **Delegated → another account**
(a co-signer / treasury). But scoping the CI key is still on you.
5. **perch: describe · prove · enforce** — first, the account's **full policy**:
several rules where perch (the CI rule) **composes with OZ-native policies**
(policy-free admin, an OZ spending-limit cap, a post-quantum co-signer). Then,
on the CI rule:
- **build** the policy as data → a live **policy builder**: toggle the
functions the key may call, the `args[1] = self` author guard, and an
`not-after-ledger` expiry, and watch the wire `PolicyDoc`, its `doc_hash`,
the **reachable calls**, and a safety read all re-derive on every change;
- **narrow** it safely → attenuation is a *machine-checked subset* (perch
accepts a narrowing, refuses a widening);
- **enforce** it on real testnet → the CI key `post`s (allowed) but cannot
`clear` (denied by perch), with real tx links.
6. **Add signers · M-of-N** — the same account model also holds **several
co-signers** and can require a **quorum**. The **policy panel** (the signers ×
rules matrix from Act 5) gains a `2-of-3` rule via Nido's **OZ multisig
policy** — perch scopes *what* a key may do; the threshold policy governs *how
many* must sign, **composed on one account**. Proven live: `post` signed by
**2 of 3** succeeds, signed by **1** is denied on-chain.

Closes on why perch is easier *and* safer: one interpreter audited once,
INV-1/INV-2, machine-checked attenuation, and `doc_hash` = exactly what enforces —
and both perch and the multisig policy are just policies on ContextRules, no
bespoke account code.

## Run it

```sh
npm install # from the repo root (workspaces)
npm run dev -w perch-status-onchain
```

Click through the acts. In Act 5, **build** the CI grant with the toggles (watch
the `PolicyDoc`, `doc_hash`, and reachable calls update live), **Narrow →
publish-only** (accepted), **Try to widen** (refused), then **Publish** (allowed
on-chain) and **Wipe** (denied on-chain). In Act 6, **Sign with 2** (allowed —
quorum met) and **Sign with 1** (denied on-chain — below threshold). Fees are
paid by an ephemeral friendbot account funded on demand.

## Verify (browser snapshots)

```sh
npx playwright install chromium
npm run test:e2e -w perch-status-onchain # LIVE testnet — funds + submits real txs
```

Walks all six acts, exercises attenuation, drives real `post`/`clear`
transactions, and proves the 2-of-3 quorum live; writes `artifacts/*.png`. Needs
network + testnet, so it is **not** in the offline CI lane — run it explicitly.

## Deployed testnet pieces

| contract | address |
|---|---|
| perch interpreter (OZ Policy) | `CBO4FIGR2LP242IKWDME6NPFGCFAT5R7CSLKYLOOJFVXCCIGKVF6O44G` |
| status board (`post`/`clear`/`get`) | `CBVXSCMALSZBF32OGUXIXFAFMPYFOJM4BOA27PBCMJPR6ZNUREX5ELWM` |
| WebAuthn verifier (secp256r1) | `CACVGSAHYFBXY4LJKWW5B57LAAXHCZVDZOANUTYPLNV6HHQI4Q35EGMY` |
| perch-governed Nido account (Act 5) | `CAZSVYNP52AGK66S3XIAW6HJDFLMXHH3IQECRNCWKHSPIXKMD4RBNMPV` |
| Nido multisig policy (Act 6) | `CCSDKJYOFCPTCCGQZPF73RJNHFC7TPO532Q36N3M2VBYZFWQOTDB7J7G` |
| 2-of-3 quorum account (Act 6) | `CCJLM2X6SDBX5QXFI7QCZ42Q3TAYWBWYA2IG56IUHLXRNQIKP4OU3GQL` |

`interpreter wasm hash d0f93aac… · account wasm hash 5bb9f585… · doc_hash 7e6b00a4…`

The Act-6 quorum is proven live — `post` on the 2-of-3 account signed by **2 of 3**
co-signers is [allowed](https://stellar.expert/explorer/testnet/tx/21302c3eab6037c8c2f562a69b57ce42940fb161cb264da5c28a69722ddcb34b)
(`21302c3e…`); signed by **1** it is [denied on-chain](https://stellar.expert/explorer/testnet/tx/6f0265f978ab48fafc9753bd8a892351e701fc67f6183b40acea7fb5da45dde4)
(`6f0265f9…`, `FAILED`). `scripts/prove-threshold.ts` deploys the account and
reproduces both.

## How the policy gets on-chain

The RPN lowering (`PolicyDoc → InstallParams`) is Rust-only, so the policy is
compiled by the `perch-plan` CLI (perch repo) and installed as the account's
Default-rule policy at construction. `scripts/deploy-and-prove.ts` does the whole
thing (compile → deploy → prove allow+deny) and is the source of truth for the
addresses above. `src/perchOnchain.ts` holds the invoke flow used by Acts 5–6.

Act 6's quorum account is orthogonal: no perch policy, just Nido's **multisig
policy** (`SimpleThresholdAccountParams { threshold: 2 }`) on the Default rule
over three secp256r1 co-signers. `scripts/prove-threshold.ts` deploys it and the
browser drives the multi-signer ceremony live — M assertions over one auth digest
land in a single `AuthPayload` via `injectSignedAuthPayload`. perch scopes *what*
a key may do; the threshold policy governs *how many* must sign — composed on one
account, no bespoke account code either way.

### The footprint gotcha

Recording `simulateTransaction` never runs `__check_auth`, so its footprint
omits what the account's auth check touches — the verifier code, the interpreter
code, and the read-**write** `Program(account, rule_id)` entry perch `extend_ttl`s
— failing with `scecExceededLimit`. Fixed by **re-simulating the signed tx**
(enforcing mode) and submitting with that footprint. Every OZ/perch/nido test
mocks auth, so this path was untested until here.

## Roadmap — v2: author policies in the browser

Today the policy is fixed (compiled by the Rust CLI at deploy time). Next: a
WebAssembly build of the perch compiler so a policy built in Act 5 can be
compiled in-browser (byte-identical to Rust) and deployed live.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 23 additions & 0 deletions examples/perch-status-onchain/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="color-scheme" content="light" />
<meta name="theme-color" content="#FFF8F0" />
<title>Scope a key with perch — a Nido guided tour</title>
<!-- Nido "Nest Ring": dashed coral + honey rings around a teal dot. -->
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 120'%3E%3Crect width='120' height='120' rx='26' fill='%23FFF8F0'/%3E%3Cg fill='none' stroke-width='6' stroke-linecap='round'%3E%3Ccircle cx='60' cy='60' r='46' stroke='%23F25C2A' stroke-dasharray='14 9'/%3E%3Ccircle cx='60' cy='60' r='31' stroke='%23F5A623' stroke-dasharray='11 8'/%3E%3C/g%3E%3Ccircle cx='60' cy='60' r='11' fill='%230E9AA8'/%3E%3C/svg%3E" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,400;0,9..144,600;0,9..144,800;1,9..144,400;1,9..144,600;1,9..144,800&family=Hanken+Grotesk:wght@400..800&family=JetBrains+Mono:wght@400..600&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="/src/styles.css" />
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
28 changes: 28 additions & 0 deletions examples/perch-status-onchain/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "perch-status-onchain",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"build:pages": "tsc && vite build --base=/${REPO_NAME:-nido}/",
"build:preview": "tsc && vite build --base=./",
"preview": "vite preview",
"typecheck": "tsc --noEmit",
"test:e2e": "playwright test",
"prove": "tsx scripts/deploy-and-prove.ts"
},
"dependencies": {
"@nidohq/passkey-sdk": "*",
"@nidohq/testkit": "*",
"@stellar/stellar-sdk": "^15.1.0"
},
"devDependencies": {
"@playwright/test": "^1.50.0",
"tsx": "^4.19.0",
"typescript": "~5.9.3",
"vite": "^7.3.1",
"vite-plugin-node-polyfills": "^0.25.0"
}
}
21 changes: 21 additions & 0 deletions examples/perch-status-onchain/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
testDir: './tests',
fullyParallel: false,
reporter: [['list']],
timeout: 120_000,
use: {
baseURL: 'http://localhost:5178',
viewport: { width: 1200, height: 1500 },
// Settle entrance animations so full-page snapshots aren't caught mid-fade.
reducedMotion: 'reduce',
},
webServer: {
command: 'npx vite --port 5178 --strictPort',
url: 'http://localhost:5178',
reuseExistingServer: !process.env.CI,
timeout: 60_000,
},
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
});
Loading
Loading