From e755c8d6d733df67c1d59fabd42fdad1340a0d6c Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 5 Oct 2026 17:30:56 +0200 Subject: [PATCH 01/50] feat(templates): add the base Vite + React template The ground every scaffolded project starts from: a Vite + React app on the three libraries at ^0.5.3, with ThemeProvider, CantonConnectProvider and WalletButton wired in App.tsx. A mock wallet behind VITE_MOCK_WALLET lets it connect with nothing reachable, and a Wallet Gateway joins the picker when VITE_WALLET_GATEWAY_URL names one. _gitignore and _pnpm-workspace.yaml travel renamed: npm drops a .gitignore from every tarball it packs, and a pnpm-workspace.yaml inside the monorepo would make pnpm treat the template as its own workspace root. The scaffolder renames them on copy. customConditions: ["development"] in tsconfig.app.json is what lets the monorepo typecheck the template against the libraries' source before any dist exists, the same way dapp/frontend does. A published package has no such condition, so in a scaffolded project the setting is inert. --- templates/base/.env.example | 7 ++++ templates/base/README.md | 30 +++++++++++++++++ templates/base/_gitignore | 6 ++++ templates/base/_pnpm-workspace.yaml | 4 +++ templates/base/index.html | 15 +++++++++ templates/base/package.json | 29 ++++++++++++++++ templates/base/src/App.tsx | 52 +++++++++++++++++++++++++++++ templates/base/src/index.css | 16 +++++++++ templates/base/src/main.tsx | 15 +++++++++ templates/base/src/vite-env.d.ts | 6 ++++ templates/base/tsconfig.app.json | 27 +++++++++++++++ templates/base/tsconfig.json | 4 +++ templates/base/tsconfig.node.json | 24 +++++++++++++ templates/base/vite.config.ts | 6 ++++ 14 files changed, 241 insertions(+) create mode 100644 templates/base/.env.example create mode 100644 templates/base/README.md create mode 100644 templates/base/_gitignore create mode 100644 templates/base/_pnpm-workspace.yaml create mode 100644 templates/base/index.html create mode 100644 templates/base/package.json create mode 100644 templates/base/src/App.tsx create mode 100644 templates/base/src/index.css create mode 100644 templates/base/src/main.tsx create mode 100644 templates/base/src/vite-env.d.ts create mode 100644 templates/base/tsconfig.app.json create mode 100644 templates/base/tsconfig.json create mode 100644 templates/base/tsconfig.node.json create mode 100644 templates/base/vite.config.ts diff --git a/templates/base/.env.example b/templates/base/.env.example new file mode 100644 index 00000000..288ddc0d --- /dev/null +++ b/templates/base/.env.example @@ -0,0 +1,7 @@ +# Offers a "Mock Wallet" in the wallet picker so the app connects with no wallet reachable. +# Unset it, or set it to anything but `true`, once you have a real wallet to connect. +VITE_MOCK_WALLET=true + +# Lists a Wallet Gateway (https://github.com/canton-network/wallet) in the picker. A session +# survives a reload only for a gateway listed here. +# VITE_WALLET_GATEWAY_URL=http://localhost:3030/api/v0/dapp diff --git a/templates/base/README.md b/templates/base/README.md new file mode 100644 index 00000000..d7cfdcd5 --- /dev/null +++ b/templates/base/README.md @@ -0,0 +1,30 @@ +# Canton dApp + +A React + Vite app wired to the Canton dApp kit: + +- `@bootnodedev/canton-connect` — wallet session and hooks +- `@bootnodedev/canton-dappbooster` — headless components +- `@bootnodedev/canton-theme` — the default stylesheet, imported once in `src/index.css` + +## Run + +```bash +cp .env.example .env +pnpm install +pnpm dev +``` + +The app serves on http://localhost:3012. With `VITE_MOCK_WALLET=true` the wallet picker offers a +mock wallet, so you can connect before a real one is reachable. + +## Connect a real wallet + +Set `VITE_WALLET_GATEWAY_URL` to a [Wallet Gateway](https://github.com/canton-network/wallet) and +it appears in the picker, which also lists any CIP-0103 browser wallet it finds in the page. Unset +`VITE_MOCK_WALLET` once the mock is no longer needed. + +## Next + +- Hooks: `useAccount`, `useLedger`, `useExecute`, `useSignMessage` from `@bootnodedev/canton-connect` +- Components: `Identifier`, `PartyIdInput`, `TokenInput`, `ExplorerLink` from `@bootnodedev/canton-dappbooster` +- Source and docs: https://github.com/BootNodeDev/canton-dappbooster diff --git a/templates/base/_gitignore b/templates/base/_gitignore new file mode 100644 index 00000000..8cf973de --- /dev/null +++ b/templates/base/_gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +*.tsbuildinfo +.env +.env.local +.DS_Store diff --git a/templates/base/_pnpm-workspace.yaml b/templates/base/_pnpm-workspace.yaml new file mode 100644 index 00000000..8b861fbd --- /dev/null +++ b/templates/base/_pnpm-workspace.yaml @@ -0,0 +1,4 @@ +# pnpm 10+ refuses to run a dependency's build script until it is listed here. protobufjs, under +# the Canton SDK, has one; without this entry every install warns about it. +allowBuilds: + protobufjs: true diff --git a/templates/base/index.html b/templates/base/index.html new file mode 100644 index 00000000..d4cf92cd --- /dev/null +++ b/templates/base/index.html @@ -0,0 +1,15 @@ + + + + + + + Canton dApp + + + + +
+ + + diff --git a/templates/base/package.json b/templates/base/package.json new file mode 100644 index 00000000..a15e60de --- /dev/null +++ b/templates/base/package.json @@ -0,0 +1,29 @@ +{ + "name": "@bootnodedev/canton-template-base", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite --host localhost --port 3012 --strictPort", + "build": "tsc -b && vite build", + "preview": "vite preview", + "typecheck": "tsc -b --noEmit" + }, + "dependencies": { + "@bootnodedev/canton-connect": "^0.5.3", + "@bootnodedev/canton-dappbooster": "^0.5.3", + "@bootnodedev/canton-theme": "^0.5.3", + "@canton-network/core-types": "^1.8.0", + "@canton-network/dapp-sdk": "1.5.1", + "@walletconnect/sign-client": "^2.23.9", + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@types/react": "^19.1.0", + "@types/react-dom": "^19.1.0", + "@vitejs/plugin-react": "^6.0.0", + "typescript": "^5.9.3", + "vite": "^8.0.0" + } +} diff --git a/templates/base/src/App.tsx b/templates/base/src/App.tsx new file mode 100644 index 00000000..e92a141f --- /dev/null +++ b/templates/base/src/App.tsx @@ -0,0 +1,52 @@ +import { + type CantonConnectConfig, + CantonConnectProvider, + createMockAdapter, + useAccount, +} from '@bootnodedev/canton-connect' +import { Identifier, ThemeProvider } from '@bootnodedev/canton-dappbooster' +import { WalletButton } from '@bootnodedev/canton-dappbooster/connect' +import { RemoteAdapter } from '@canton-network/dapp-sdk' +import type { ReactElement } from 'react' + +// The mock wallet answers the connect flow and nothing else, so the app runs before a real +// wallet is reachable. A Wallet Gateway is listed when .env names one. See .env.example. +const mockWallet = import.meta.env.VITE_MOCK_WALLET === 'true' ? [createMockAdapter()] : [] +const gatewayUrl = import.meta.env.VITE_WALLET_GATEWAY_URL +const gateway = gatewayUrl + ? [new RemoteAdapter({ name: 'Wallet Gateway', rpcUrl: gatewayUrl })] + : [] + +const connectConfig: CantonConnectConfig = { + appName: 'Canton dApp', + additionalAdapters: [...mockWallet, ...gateway], +} + +const Session = (): ReactElement | null => { + const { account, status } = useAccount() + + // `idle` is "not determined yet": the provider is still restoring a previous session. + if (status === 'idle') { + return null + } + if (account === undefined) { + return

No wallet connected.

+ } + return ( +

+ Connected as +

+ ) +} + +export const App = (): ReactElement => ( + + +
+

Canton dApp

+ + +
+
+
+) diff --git a/templates/base/src/index.css b/templates/base/src/index.css new file mode 100644 index 00000000..c72c75ee --- /dev/null +++ b/templates/base/src/index.css @@ -0,0 +1,16 @@ +@import "@bootnodedev/canton-theme/default.css"; + +body { + background: var(--cnc-bg); + color: var(--cnc-text); + font-family: system-ui, sans-serif; + margin: 0; +} + +main { + display: grid; + gap: 1.5rem; + margin: 0 auto; + max-width: 40rem; + padding: 4rem 1rem; +} diff --git a/templates/base/src/main.tsx b/templates/base/src/main.tsx new file mode 100644 index 00000000..c3b9abe7 --- /dev/null +++ b/templates/base/src/main.tsx @@ -0,0 +1,15 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import { App } from './App' +import './index.css' + +const rootEl = document.getElementById('root') +if (rootEl === null) { + throw new Error('Root element #root not found') +} + +createRoot(rootEl).render( + + + , +) diff --git a/templates/base/src/vite-env.d.ts b/templates/base/src/vite-env.d.ts new file mode 100644 index 00000000..95bdc39f --- /dev/null +++ b/templates/base/src/vite-env.d.ts @@ -0,0 +1,6 @@ +/// + +interface ImportMetaEnv { + readonly VITE_MOCK_WALLET?: string + readonly VITE_WALLET_GATEWAY_URL?: string +} diff --git a/templates/base/tsconfig.app.json b/templates/base/tsconfig.app.json new file mode 100644 index 00000000..ad427244 --- /dev/null +++ b/templates/base/tsconfig.app.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + "moduleResolution": "bundler", + "customConditions": ["development"], + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/templates/base/tsconfig.json b/templates/base/tsconfig.json new file mode 100644 index 00000000..d32ff682 --- /dev/null +++ b/templates/base/tsconfig.json @@ -0,0 +1,4 @@ +{ + "files": [], + "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }] +} diff --git a/templates/base/tsconfig.node.json b/templates/base/tsconfig.node.json new file mode 100644 index 00000000..5d494bdd --- /dev/null +++ b/templates/base/tsconfig.node.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/templates/base/vite.config.ts b/templates/base/vite.config.ts new file mode 100644 index 00000000..ae745180 --- /dev/null +++ b/templates/base/vite.config.ts @@ -0,0 +1,6 @@ +import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite' + +export default defineConfig({ + plugins: [react()], +}) From 9481e3503a02b50ff0483c054b7f5461d80f2d51 Mon Sep 17 00:00:00 2001 From: fernandomg Date: Mon, 5 Oct 2026 17:30:58 +0200 Subject: [PATCH 02/50] feat(templates): add the localnet fragment Merged over the base when the scaffolder is asked for the localnet tier: dev-stack.sh and the scripts it drives (LocalNet config, DAML deps, DAR build, token minting, DAR deploy), a one-template DAML starter, the Wallet Gateway config, and a package.json fragment adding canton-barebones and wallet-gateway-remote as devDependencies with the scripts dev-stack.sh calls by name. The scripts come from this repo's scripts/. dev-stack.sh is adapted to a scaffolded project: its run dir lives inside the project, `bootstrap` runs only when the project defines it, and a port already taken names its holder and leaves that process alone. build-dar.mjs builds on the pinned Daml SDK or the newest installed patch of its minor line, as the root one does. --- .gitignore | 4 + templates/localnet/.env.example | 19 + templates/localnet/README.md | 29 + templates/localnet/_gitignore | 13 + templates/localnet/daml/Main.daml | 18 + templates/localnet/daml/daml.yaml | 12 + templates/localnet/package.json | 17 + templates/localnet/scripts/build-dar.mjs | 127 ++++ templates/localnet/scripts/deploy-dar.sh | 67 +++ templates/localnet/scripts/dev-stack.sh | 568 ++++++++++++++++++ .../localnet/scripts/fetch-daml-deps.mjs | 118 ++++ .../localnet/scripts/localnet-config.mjs | 116 ++++ templates/localnet/scripts/mint-token.mjs | 84 +++ templates/localnet/wallet-gateway.config.json | 74 +++ 14 files changed, 1266 insertions(+) create mode 100644 templates/localnet/.env.example create mode 100644 templates/localnet/README.md create mode 100644 templates/localnet/_gitignore create mode 100644 templates/localnet/daml/Main.daml create mode 100644 templates/localnet/daml/daml.yaml create mode 100644 templates/localnet/package.json create mode 100755 templates/localnet/scripts/build-dar.mjs create mode 100755 templates/localnet/scripts/deploy-dar.sh create mode 100755 templates/localnet/scripts/dev-stack.sh create mode 100644 templates/localnet/scripts/fetch-daml-deps.mjs create mode 100644 templates/localnet/scripts/localnet-config.mjs create mode 100755 templates/localnet/scripts/mint-token.mjs create mode 100644 templates/localnet/wallet-gateway.config.json diff --git a/.gitignore b/.gitignore index 637bfbf9..fdf29ff7 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,7 @@ playwright-report/ # vitest coverage output coverage/ + +# the DAML starter's build output and fetched DARs +templates/localnet/daml/.daml/ +templates/localnet/daml/deps/ diff --git a/templates/localnet/.env.example b/templates/localnet/.env.example new file mode 100644 index 00000000..e103e5e2 --- /dev/null +++ b/templates/localnet/.env.example @@ -0,0 +1,19 @@ + +############################################################################### +# Participant +############################################################################### +CANTON_JSON_API_URL=http://localhost:2975 + +############################################################################### +# Scripts / token generation +############################################################################### +CANTON_AUTH_AUDIENCE=https://canton.network.global +# Never copy CANTON_AUTH_SECRET into a browser. +CANTON_AUTH_SECRET=unsafe +# `pnpm stack up` fills this in; or run `pnpm mint-token` +CANTON_BACKEND_TOKEN= + +############################################################################### +# Wallet Gateway (pnpm wallet-gateway, or started by `pnpm stack up`) +############################################################################### +VITE_WALLET_GATEWAY_URL=http://localhost:3030/api/v0/dapp diff --git a/templates/localnet/README.md b/templates/localnet/README.md new file mode 100644 index 00000000..f271fc76 --- /dev/null +++ b/templates/localnet/README.md @@ -0,0 +1,29 @@ + +## Local network + +This project also carries a full local Canton stack: + +- `@bootnodedev/canton-barebones` runs a Splice LocalNet in Docker (config lands in `.canton-localnet/`) +- `@canton-network/wallet-gateway-remote` is the [Wallet Gateway](https://github.com/canton-network/wallet) + the dApp connects to, on http://localhost:3030 (config in `wallet-gateway.config.json`) +- `daml/` holds a starter DAML model, built with `dpm` and uploaded to the participant + +Requirements: Docker running, and the Daml SDK 3.5 (`dpm`). `daml/daml.yaml` pins the patch it is +tested on; any installed 3.5.x builds. + +```bash +pnpm stack up # LocalNet, build + deploy the DAR, Wallet Gateway, dev server +pnpm stack down # stop everything, keep the ledger +pnpm stack # interactive menu +``` + +Then pick Wallet Gateway in the dApp's picker and log in with client secret `unsafe`. Create a +party in the gateway with `wallet-kernel` as the signing provider. + +`pnpm stack up` mints `CANTON_BACKEND_TOKEN` into `.env` on first run. To wipe the ledger, run +`canton-barebones reset` inside `.canton-localnet/`; parties the gateway created must then be +created again. + +Every step is also a script of its own: `pnpm build-dar`, `pnpm deploy-dar -- daml/.daml/dist/.dar`, +`pnpm mint-token`, `pnpm wallet-gateway`. If the project defines a `bootstrap` script, `stack up` +runs it once the DAR is deployed — the place for anything your model needs created on the ledger first. diff --git a/templates/localnet/_gitignore b/templates/localnet/_gitignore new file mode 100644 index 00000000..9b23181c --- /dev/null +++ b/templates/localnet/_gitignore @@ -0,0 +1,13 @@ + +# canton-barebones: its scaffolded config, the Splice checkout and the runtime env +.canton-localnet/ + +# DAML build output and the fetched Splice DARs +daml/.daml/ +daml/deps/ + +# dev-stack.sh: logs of the background processes it starts +.dev-stack/ + +# wallet-gateway: its sqlite wallet and signing stores +.wallet-gateway/ diff --git a/templates/localnet/daml/Main.daml b/templates/localnet/daml/Main.daml new file mode 100644 index 00000000..b5ae693e --- /dev/null +++ b/templates/localnet/daml/Main.daml @@ -0,0 +1,18 @@ +module Main where + +-- A minimal template to prove the loop: build, upload, exercise from the dApp. +template Asset + with + issuer : Party + owner : Party + name : Text + where + signatory issuer + observer owner + + choice Give : ContractId Asset + with + newOwner : Party + controller owner + do + create this with owner = newOwner diff --git a/templates/localnet/daml/daml.yaml b/templates/localnet/daml/daml.yaml new file mode 100644 index 00000000..6076fdec --- /dev/null +++ b/templates/localnet/daml/daml.yaml @@ -0,0 +1,12 @@ +sdk-version: 3.5.2 +name: starter +source: . +version: 0.1.0 +dependencies: + - daml-prim + - daml-stdlib +# Splice DARs go here as `deps/.dar`; scripts/fetch-daml-deps.mjs fetches whatever is listed +# from the Splice release the LocalNet runs, and stays a no-op while the list is empty. +data-dependencies: [] +build-options: + - --target=2.1 diff --git a/templates/localnet/package.json b/templates/localnet/package.json new file mode 100644 index 00000000..18d045ac --- /dev/null +++ b/templates/localnet/package.json @@ -0,0 +1,17 @@ +{ + "name": "@bootnodedev/canton-template-localnet", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "stack": "bash scripts/dev-stack.sh", + "mint-token": "node scripts/mint-token.mjs ledger-api-user", + "build-dar": "node scripts/fetch-daml-deps.mjs && node scripts/build-dar.mjs", + "deploy-dar": "bash scripts/deploy-dar.sh", + "wallet-gateway": "mkdir -p .wallet-gateway && wallet-gateway -c ./wallet-gateway.config.json" + }, + "devDependencies": { + "@bootnodedev/canton-barebones": "0.6.0", + "@canton-network/wallet-gateway-remote": "1.11.3" + } +} diff --git a/templates/localnet/scripts/build-dar.mjs b/templates/localnet/scripts/build-dar.mjs new file mode 100755 index 00000000..687fd5e6 --- /dev/null +++ b/templates/localnet/scripts/build-dar.mjs @@ -0,0 +1,127 @@ +#!/usr/bin/env node +// +// Builds daml/ on the pinned Daml SDK, else on the newest installed patch of the pin's minor line. +import { spawnSync } from 'node:child_process' +import fs from 'node:fs' +import path from 'node:path' + +const DAML_DIR = path.resolve(import.meta.dirname, '..', 'daml') +const DPM_INSTALL_DOCS = 'https://docs.canton.network/sdks-tools/cli-tools/dpm#installation' + +export const sdkVersionOf = (damlYaml) => + damlYaml.match(/^sdk-version:\s*["']?([^\s"'#]+)["']?\s*(?:#.*)?$/m)?.[1] + +export const pinnedSdk = () => + sdkVersionOf(fs.readFileSync(path.join(DAML_DIR, 'daml.yaml'), 'utf8')) + +// dpm takes a package, a project or an SDK from these, so an inherited one points it somewhere else. +const PACKAGE_CONTEXT = [ + 'DAML_PACKAGE', + 'DAML_PROJECT', + 'DPM_ASSEMBLY', + 'DPM_LOCKFILE_ENABLED', + 'DPM_MULTI_PACKAGE', + 'DPM_RESOLUTION_FILE', + 'DPM_SDK_VERSION', +] + +export const withoutPackageContext = (env) => + Object.fromEntries(Object.entries(env).filter(([name]) => !PACKAGE_CONTEXT.includes(name))) + +export const installedSdks = (entries) => + entries + .filter((entry) => entry.installed === true && /^\d+\.\d+\.\d+$/.test(entry.version)) + .map((entry) => entry.version) + +const parsedListing = (output) => { + try { + return JSON.parse(output) + } catch { + return undefined + } +} + +const minorLine = (version) => version.split('.').slice(0, 2).join('.') + +export const chooseSdk = (installed, pin) => { + // The pin first, so a rebuild after `dpm install ` lands on it instead of the patch that failed. + if (installed.includes(pin)) { + return pin + } + + return installed + .filter((version) => minorLine(version) === minorLine(pin)) + .sort((a, b) => a.localeCompare(b, undefined, { numeric: true })) + .at(-1) +} + +const PREFIX = 'build-dar: ' + +// Bold red on a terminal only, like dev-stack.sh's `[x]`, so piped output stays plain. +const errorPrefix = () => + process.stderr.isTTY ? `\u001b[1;31m${PREFIX.trimEnd()}\u001b[0m ` : PREFIX + +const fail = (...lines) => { + console.error(errorPrefix() + lines.join(`\n${' '.repeat(PREFIX.length)}`)) + process.exit(1) +} + +const main = () => { + const pin = pinnedSdk() + if (pin === undefined) { + fail('no sdk-version in daml/daml.yaml.') + } + + const listing = spawnSync('dpm', ['version', '--output', 'json'], { + cwd: '/', // always exists, and no daml.yaml above it gives dpm a package context + encoding: 'utf8', + env: withoutPackageContext(process.env), + }) + if (listing.error?.code === 'ENOENT') { + fail( + 'dpm not found on PATH.', + `Install it and add ~/.dpm/bin to your PATH: ${DPM_INSTALL_DOCS}`, + ) + } + if (listing.error !== undefined) { + fail(`dpm could not run: ${listing.error.code}.`, `Reinstall it: ${DPM_INSTALL_DOCS}`) + } + if (listing.status !== 0) { + fail('`dpm version` failed:', ...(listing.stderr || listing.stdout).trim().split('\n')) + } + + const entries = parsedListing(listing.stdout) + if (!Array.isArray(entries)) { + fail('could not read the SDK list from `dpm version --output json`.') + } + + const sdk = chooseSdk(installedSdks(entries), pin) + if (sdk === undefined) { + fail(`no Daml SDK ${minorLine(pin)} installed.`, `Install the tested one: dpm install ${pin}`) + } + + console.log(`${PREFIX}building on Daml SDK ${sdk}`) + const build = spawnSync('dpm', ['build'], { + cwd: DAML_DIR, + stdio: 'inherit', + env: { + ...withoutPackageContext(process.env), + DAML_PACKAGE: DAML_DIR, + DPM_SDK_VERSION: sdk, // undocumented: overrides daml.yaml's sdk-version for this build only + LANG: 'C.UTF-8', + }, + }) + if (build.status === 0) { + return + } + + if (sdk === pin) { + fail(`the build failed on Daml SDK ${sdk}.`) + } + fail(`the build failed on Daml SDK ${sdk}.`, `It is tested on ${pin}: dpm install ${pin}`) +} + +// `import.meta.filename` rather than a `file://` template around argv[1], as in fetch-daml-deps.mjs. +if (import.meta.filename === process.argv[1]) { + main() +} diff --git a/templates/localnet/scripts/deploy-dar.sh b/templates/localnet/scripts/deploy-dar.sh new file mode 100755 index 00000000..1cb50b99 --- /dev/null +++ b/templates/localnet/scripts/deploy-dar.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +set -euo pipefail + +# pnpm 11 forwards the `--` separator to the script; drop it so `pnpm run +# deploy-dar -- ` and a direct `deploy-dar.sh ` both work. +[ "${1:-}" = "--" ] && shift + +if [ "$#" -ne 1 ]; then + echo "Usage: scripts/deploy-dar.sh path/to/file.dar" >&2 + exit 1 +fi + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +# Captured before sourcing so a value the caller exported beats .env, the same precedence +# mint-token.mjs uses. No `set -a`: curl takes the token as a header, so exporting the +# whole file would only hand CANTON_AUTH_SECRET to a child that has no use for it. +preset_token="${CANTON_BACKEND_TOKEN:-}" +preset_json_api_url="${CANTON_JSON_API_URL:-}" + +if [ -f "$ROOT/.env" ]; then + # shellcheck disable=SC1091 + source "$ROOT/.env" +fi + +token="${preset_token:-${CANTON_BACKEND_TOKEN:-}}" +# The same name .env and dev-stack.sh use, so retargeting the participant moves the +# upload with it rather than only moving one caller. +json_api_url="${preset_json_api_url:-${CANTON_JSON_API_URL:-http://localhost:2975}}" + +DAR_PATH="$1" + +if [ ! -f "$DAR_PATH" ]; then + echo "DAR not found: $DAR_PATH" >&2 + exit 1 +fi + +if [ -z "$token" ]; then + echo "CANTON_BACKEND_TOKEN is required. Generate one with: pnpm run mint-token" >&2 + exit 1 +fi + +dar_name="$(basename "$DAR_PATH")" + +echo "Uploading $dar_name to app-user JSON API at $json_api_url" + +http_code="$( + curl -sS -o /dev/null -w '%{http_code}' \ + -X POST \ + "$json_api_url/v2/packages" \ + -H "Authorization: Bearer $token" \ + -H "Content-Type: application/octet-stream" \ + --data-binary "@$DAR_PATH" +)" + +case "$http_code" in + 200 | 204) + echo "deployed $dar_name to app-user" + ;; + 409) + echo "$dar_name already deployed to app-user" + ;; + *) + echo "DAR upload failed with HTTP $http_code" >&2 + exit 1 + ;; +esac diff --git a/templates/localnet/scripts/dev-stack.sh b/templates/localnet/scripts/dev-stack.sh new file mode 100755 index 00000000..20c44c67 --- /dev/null +++ b/templates/localnet/scripts/dev-stack.sh @@ -0,0 +1,568 @@ +#!/usr/bin/env bash +# +# dev-stack.sh — start or stop the local Canton dApp stack. +# +# Every step is a script in package.json, which is the contract this file relies on: +# `mint-token`, `build-dar`, `deploy-dar`, `wallet-gateway`, `dev`, and optionally `bootstrap`. +# +# The LocalNet belongs to @bootnodedev/canton-barebones, pinned in +# package.json and driven from the directory holding its config. `up` scaffolds that +# directory itself, at the gitignored ./.canton-localnet; point elsewhere with a +# second argument or with CANTON_LOCALNET_DIR, in that order of precedence. +# +# Docker lifecycle is managed separately from the stack: start/quit Docker with +# `docker-up` / `docker-down` (macOS only), the Docker app, or your CLI. `up` +# and `down` assume Docker is already running and never start or quit it. +# +# Usage: +# ./scripts/dev-stack.sh [dir] # interactive arrow-key menu (default) +# ./scripts/dev-stack.sh menu [dir] # same as above +# ./scripts/dev-stack.sh install # pnpm install +# ./scripts/dev-stack.sh docker-up # macOS only: launch Docker Desktop, wait for the daemon +# ./scripts/dev-stack.sh up [dir] # start the stack (LocalNet, DAR, bootstrap, gateway, dApp) +# ./scripts/dev-stack.sh down [dir] # stop the gateway + the dApp dev server, stop the LocalNet +# ./scripts/dev-stack.sh docker-down # macOS only: quit Docker Desktop +# ./scripts/dev-stack.sh status [dir] # show what is currently running +# +# [dir] is the LocalNet directory, and every menu action uses it. +# +# Flags, for `up` only and never both at once: +# --json one JSON object per line on stdout, the human log on stderr. A step +# opens with 'start' and closes with 'ok' or 'error'; the run itself +# closes with 'done', so the stream never stops without saying why +# --quiet only warnings, errors and the closing 'Stack is up:' block +# +# `up` runs numbered steps, ending with the Wallet Gateway on 3030 and the dApp dev server +# on 3012 in the background; the `step` calls in up() are the list. `down` kills both and +# stops the LocalNet, keeping its volumes. + +set -euo pipefail + +# Resolve the project root from this script's location so it works from any cwd. +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +cd "$ROOT_DIR" + +# Inside the project, so two projects driving a stack never share logs or step on each other. +RUN_DIR="$ROOT_DIR/.dev-stack" +DAPP_LOG="$RUN_DIR/dapp-dev.log" +DAPP_PID="$RUN_DIR/dapp-dev.pid" +GW_LOG="$RUN_DIR/wallet-gateway.log" +GW_PID="$RUN_DIR/wallet-gateway.pid" + +# Resolved in up(), once ./.env has been read. +JSON_API_URL="" + +# Derive the DAR name from daml.yaml so renames and version bumps need no edit here. +DAML_DIR="${DAML_DIR:-daml}" +DAR_NAME="$(awk '/^name:/{n=$2} /^version:/{v=$2} END{print n"-"v".dar"}' "$DAML_DIR/daml.yaml")" +DAR_PATH="$DAML_DIR/.daml/dist/$DAR_NAME" + +# Flags are pulled out before the positional arguments, so `up --json ` and +# `up --json` both work. +JSON_MODE=0 +QUIET_MODE=0 +ARGS=() +for arg in "$@"; do + case "$arg" in + --json) JSON_MODE=1 ;; + --quiet) QUIET_MODE=1 ;; + *) ARGS+=("$arg") ;; + esac +done +set -- ${ARGS[@]+"${ARGS[@]}"} + +# Four sinks: fd 3 always shows, fd 5 is the chatter --quiet drops, fd 4 is the machine +# stream, and fd 1 is what child processes inherit. --json moves every human byte to +# stderr so nothing but JSON reaches stdout; --quiet drops the child output instead. +exec 3>&1 4>&1 +if [ "$JSON_MODE" = 1 ]; then exec 3>&2 1>&2; fi +exec 5>&3 +if [ "$QUIET_MODE" = 1 ]; then exec 5>/dev/null 1>/dev/null; fi + +if [ -t 3 ]; then + HUMAN_TTY=1 + P_STEP=$'\033[1;36m==>\033[0m' P_WARN=$'\033[1;33m[!]\033[0m' P_ERR=$'\033[1;31m[x]\033[0m' +else + HUMAN_TTY=0 + P_STEP='==>' P_WARN='[!]' P_ERR='[x]' +fi + +log() { printf '%s %s\n' "$P_STEP" "$*" >&5; } +say() { printf '%s %s\n' "$P_STEP" "$*" >&3; } +warn() { printf '%s %s\n' "$P_WARN" "$*" >&3; } +# The one human line outside the fd scheme: an error belongs on stderr in every mode. +die() { + local message="$*" + json_event error "${message//$'\n'/ }" + printf '%s %s\n' "$P_ERR" "${message//$'\n'/$'\n' }" >&2 + exit 1 +} + +# Keep in step with the `step` calls in up(), which are the list. +STEP_TOTAL=10 +STEP_INDEX=0 +STEP_NAME=stack +STEP_START=$SECONDS + +json_event() { # json_event [message] + [ "$JSON_MODE" = 1 ] || return 0 + local message + printf '{"step":"%s","index":%d,"total":%d,"status":"%s","elapsed":%d' \ + "$STEP_NAME" "$STEP_INDEX" "$STEP_TOTAL" "$1" "$((SECONDS - STEP_START))" >&4 + if [ -n "${2:-}" ]; then + message="${2//\\/\\\\}" + printf ',"message":"%s"' "${message//\"/\\\"}" >&4 + fi + printf '}\n' >&4 +} + +step() { # step + [ "$STEP_INDEX" -eq 0 ] || json_event ok + STEP_INDEX=$((STEP_INDEX + 1)) + STEP_NAME="$1" + STEP_START=$SECONDS + log "[$STEP_INDEX/$STEP_TOTAL] $2" + json_event start +} + +# A budgeted wait looks the same as a hang, so it counts out loud. On a terminal the +# counter rewrites one line; piped, it prints every 30s so the log stays readable. +TICK_LAST=0 + +tick() { # tick