diff --git a/.github/workflows/check-external-links.yml b/.github/workflows/check-external-links.yml new file mode 100644 index 000000000..b2f50f6d4 --- /dev/null +++ b/.github/workflows/check-external-links.yml @@ -0,0 +1,37 @@ +name: Check external links + +# External links rot on their own schedule, independent of any PR, so this +# runs weekly rather than gating every push/PR (it would also make every PR +# build flaky against third-party outages and bot-wall rate limits that have +# nothing to do with the change under review). + +on: + schedule: + - cron: '17 6 * * 1' + workflow_dispatch: + +env: + NODE_NEW_VERSION: 'lts/jod' + +jobs: + check-external-links: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Use Node.js ${{ env.NODE_NEW_VERSION }} + uses: actions/setup-node@v3 + with: + node-version: ${{ env.NODE_NEW_VERSION }} + + - name: Install + run: | + corepack enable + yarn install + + # Fails only on a confirmed 404/410/5xx or a genuine network error; a + # site that bot-walls scripted requests (401/403/429) is reported, not + # treated as broken. See scripts/checkExternalLinks.mjs. + - name: Check links + run: yarn lint:check-external-links diff --git a/.github/workflows/test-markdown.yml b/.github/workflows/test-markdown.yml index b361b9065..038e22e9e 100644 --- a/.github/workflows/test-markdown.yml +++ b/.github/workflows/test-markdown.yml @@ -40,3 +40,8 @@ jobs: corepack enable yarn install yarn lint:format + + # Every code example a page shows literally must still match the + # checked-in, test-exercised file it was excerpted from. + - name: Check code-example provenance + run: yarn lint:excerpts diff --git a/.prettierrc.json b/.prettierrc.json index 2ef00902e..58f8d2ab0 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -4,7 +4,7 @@ "useTabs": false, "singleQuote": true, "proseWrap": "preserve", - "trailingComma": "none", + "trailingComma": "all", "bracketSpacing": true, "arrowParens": "avoid", "overrides": [ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 525c11c0e..5795bf5cb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -105,8 +105,8 @@ Turn it on for the whole site: // .vitepress/config.js module.exports = { themeConfig: { - sidebar: 'auto' - } + sidebar: 'auto', + }, }; ``` @@ -122,17 +122,17 @@ module.exports = { link: '/foo/', // optional, link of the title, which should be an absolute path and must exist collapsable: false, // optional, defaults to true sidebarDepth: 1, // optional, defaults to 1 - items: [{ text: '', link: '/' }] + items: [{ text: '', link: '/' }], }, { title: 'Group 2', items: [ /* ... */ ], - initialOpenGroupIndex: -1 // optional, defaults to 0, defines the index of initially opened subgroup - } - ] - } + initialOpenGroupIndex: -1, // optional, defaults to 0, defines the index of initially opened subgroup + }, + ], + }, }; ``` @@ -177,7 +177,12 @@ These GitHub Actions run on every pull request and commit to `main`: - Test the build - Tests for build errors and does HTML5 validation. - Lint and Test Snippets - - Tests the imported code snippets. Code included inline is **not** tested or run. + - Tests the code snippet files under `snippets/`. +- Lint Markdown + - Formats and style-checks the Markdown, and runs the code-example + provenance check (`yarn lint:excerpts`), which verifies that every + literal code block carrying an `` annotation still + matches the checked-in, test-exercised file it was excerpted from. - Spellcheck - **Does not work yet. Until finished, please check your docs in an external spellchecker before merging.** - Checks Markdown files against a dictionary and a local wordlist. @@ -264,6 +269,69 @@ To test your snippets files while writing your docs: Note that the PR will automatically test and lint files. The above is for local testing while you're writing the docs. +### Provenance of inline code examples + +Every code example a page shows should come from a file that is checked in +and exercised by a test, so a reader can trust that the code on the page +actually runs. There are two ways to get there, and the first is preferred. + +**1. Transclude it.** `<<< @/../snippets/#` puts the source +file's region on the page at build time. The page never holds a copy, so the +example cannot drift from its source. Use this whenever you can. + +**2. Annotate a literal fence.** Transclusion does not work everywhere: a +fence nested inside a list item or a container directive has to be a literal +fenced block, and sometimes a page reads better when its Markdown source +carries the code. A literal fence can drift, so it carries an invisible +provenance annotation naming the file and region it came from: + + + + ```js + import { M } from '@endo/patterns'; + + const CounterI = M.interface('Counter', { + increment: M.call().optional(M.number()).returns(M.number()), + }); + ``` + +The annotation is an HTML comment, so it never renders. The region it names +is delimited in the source file by the same `// #region name` and +`// #endregion name` markers transclusion uses. + +`yarn lint:excerpts` (run in CI by the Lint Markdown workflow) then checks +each annotated fence: + +1. the named file and region exist; +2. the fence text equals the region text, ignoring the indentation each side + carries from its own surroundings, so an excerpt that drifts from its + source fails the build; +3. the named file is exercised by the AVA suite, either because AVA collects + it (it matches `snippets/**/test-*.js`) or because a file AVA collects + reaches it through static imports or a `require.resolve()` of its path. + +Because point 3 wants a test to actually run the excerpt, an example whose +imports have to sit next to the code they support usually cannot share a +module with another example that does the same. The convention for those +files is a short `eslint-disable` header naming `import/first` and +`import/order`, and one module per example where names would otherwise +collide. `snippets/js-programming/` is worked through in full and is the +reference for the shape. + +A fence that genuinely cannot be excerpted from running code (deliberately +broken code shown as a hazard, configuration for a tool this repository does +not depend on) opts out explicitly, stating why: + + + + +**Coverage is enforced one directory at a time.** `enforcedRoots` at the top +of `scripts/check-doc-excerpts.mjs` lists the directories where every +JavaScript and TypeScript fence must be annotated or exempt. Outside those +directories an unannotated fence is only counted and reported, so the +convention can be adopted page by page. When you finish converting a +directory, add it to `enforcedRoots` so it cannot regress. + ### Check Links To check internal VitePress links locally, run the shell command `yarn docs:build` from anywhere in the @@ -545,25 +613,25 @@ export default ({ router }) => { { path: '/wallet-api.html', redirect: '/guides/wallet/' }, { path: '/chainlink-integration/', - redirect: '/guides/chainlink-integration.md' + redirect: '/guides/chainlink-integration.md', }, { path: '/chainlink-integration.html', - redirect: '/guides/chainlink-integration.md' + redirect: '/guides/chainlink-integration.md', }, { path: '/distributed-programming/', redirect: '/guides/js-programming/' }, { path: '/distributed-programming.html', - redirect: '/guides/js-programming/' + redirect: '/guides/js-programming/', }, { path: '/getting-started/agoric-cli-guide/', - redirect: '/guides/agoric-cli/' + redirect: '/guides/agoric-cli/', }, { path: '/getting-started/agoric-cli-guide.html', - redirect: '/guides/agoric-cli/' - } + redirect: '/guides/agoric-cli/', + }, ]); }; ``` diff --git a/_redirects b/_redirects index 1d99c0875..11c32f71e 100644 --- a/_redirects +++ b/_redirects @@ -36,8 +36,11 @@ /getting-started/start-a-project.html /guides/getting-started/ /getting-started/start-a-project/* /guides/getting-started/start-a-project/:splat /guides/agoric-cli/commands /guides/agoric-cli/ -/guides/agoric-cli/commands.html /guides/agoric-cli/ +/guides/agoric-cli/commands.html /guides/agoric-cli/ /guides/agoric-cli/commands/* /guides/agoric-cli/:splat +/guides/getting-started/deploying /guides/getting-started/explainer-deploying-a-smart-contact +/guides/getting-started/deploying.html /guides/getting-started/explainer-deploying-a-smart-contact +/guides/getting-started/deploying/* /guides/getting-started/explainer-deploying-a-smart-contact:splat /guides/js-programming/ses/lockdown https://github.com/endojs/endo/blob/HEAD/packages/ses/docs/lockdown.md /guides/js-programming/ses/lockdown.html https://github.com/endojs/endo/blob/HEAD/packages/ses/docs/lockdown.md /guides/js-programming/ses/ses-guide https://github.com/endojs/endo/blob/HEAD/packages/ses/docs/guide.md diff --git a/main/.vitepress/config.mjs b/main/.vitepress/config.mjs index 89fd8414d..f6f45e598 100644 --- a/main/.vitepress/config.mjs +++ b/main/.vitepress/config.mjs @@ -136,6 +136,10 @@ export default defineConfig({ text: 'Key Concepts and APIs', link: '/guides/orchestration/key-concepts', }, + { + text: 'Async Flow', + link: '/guides/orchestration/async-flow', + }, { text: 'Contract Walkthroughs', link: '/guides/orchestration/contract-walkthroughs/', @@ -147,8 +151,8 @@ export default defineConfig({ { text: 'Cross-Chain Unbond Example', link: '/guides/orchestration/contract-walkthroughs/cross-chain-unbond', - } - ] + }, + ], }, { text: 'Example Orchestration DApp', @@ -165,14 +169,14 @@ export default defineConfig({ { text: 'UI Walkthrough', link: '/guides/orchestration/orchestration-basics/ui', - } - ] + }, + ], }, { text: 'How Orchestration Works', link: '/guides/orchestration/how-orch-works', }, - ] + ], }, { text: 'Build', @@ -184,18 +188,15 @@ export default defineConfig({ items: [ { text: 'Takeaway 1: Starting a Local Chain', - link: - '/guides/getting-started/explainer-how-to-start-a-local-chain', + link: '/guides/getting-started/explainer-how-to-start-a-local-chain', }, { text: 'Takeaway 2: Deploying a Smart Contract', - link: - '/guides/getting-started/explainer-deploying-a-smart-contact', + link: '/guides/getting-started/explainer-deploying-a-smart-contact', }, { text: 'Takeaway 3: Making an Offer', - link: - '/guides/getting-started/explainer-how-to-make-an-offer', + link: '/guides/getting-started/explainer-how-to-make-an-offer', }, ], }, @@ -208,6 +209,10 @@ export default defineConfig({ text: 'Hello World Smart Contract', link: '/guides/zoe/contract-hello', }, + { + text: 'Contract Requirements', + link: '/guides/zoe/contract-requirements', + }, { text: 'State Smart Contract', link: '/guides/zoe/contract-state', @@ -216,6 +221,18 @@ export default defineConfig({ text: 'Access Control Smart Contract', link: '/guides/zoe/contract-access-control', }, + { + text: 'The Structure of Offers', + link: '/guides/zoe/proposal', + }, + { + text: 'Offer Safety', + link: '/guides/zoe/offer-safety', + }, + { + text: 'Offer-Safety Enforcement', + link: '/guides/zoe/offer-enforcement', + }, { text: 'Complete Contract Walk-Through', link: '/guides/zoe/contract-walkthru', @@ -228,9 +245,30 @@ export default defineConfig({ text: 'Contract Upgrade', link: '/guides/zoe/contract-upgrade', }, + { + text: 'Moving Assets Between Seats', + link: '/guides/zoe/moving-assets', + }, + { + text: 'Price Authority', + link: '/guides/zoe/price-authority', + }, + { + text: 'Publishing to chainStorage', + link: '/guides/zoe/pub-to-storage', + }, { text: 'Contract Governance', link: '/guides/governance/' }, ], }, + { + text: 'Smart Wallet', + link: '/guides/wallet/', + collapsed: true, + items: [ + { text: 'Smart Wallet', link: '/guides/wallet/' }, + { text: 'Wallet UI', link: '/guides/wallet/ui' }, + ], + }, { text: 'UI Component Library', @@ -288,12 +326,12 @@ export default defineConfig({ ], }, { - text: 'Agoric API Reference', - link: 'https://agoric-sdk.pages.dev/' + text: 'Agoric API Reference', + link: 'https://agoric-sdk.pages.dev/', }, { - text: 'Endo API Reference', - link: 'https://endojs.github.io/endo/' + text: 'Endo API Reference', + link: 'https://endojs.github.io/endo/', }, { @@ -314,12 +352,36 @@ export default defineConfig({ text: 'AmountMath Object', link: '/reference/ertp-api/amount-math', }, + { + text: 'Durable Issuers', + link: '/reference/ertp-api/durable-issuers', + }, + { + text: 'Legacy Payment Helpers', + link: '/reference/ertp-api/legacy-payment-helpers', + }, { text: 'ERTP Data Types', link: '/reference/ertp-api/ertp-data-types', }, ], }, + { + text: 'Smart Wallet API', + link: '/reference/wallet-api/', + collapsed: true, + items: [ + { text: 'Smart Wallet API', link: '/reference/wallet-api/' }, + { + text: 'BridgeAction Reference', + link: '/reference/wallet-api/wallet-bridge', + }, + { + text: 'agoric wallet CLI', + link: '/reference/wallet-api/wallet-commands', + }, + ], + }, { text: 'Zoe API', link: '/reference/zoe-api/', @@ -349,6 +411,18 @@ export default defineConfig({ text: 'Zoe Data Types', link: '/reference/zoe-api/zoe-data-types', }, + { + text: 'PriceAuthority Object', + link: '/reference/zoe-api/price-authority', + }, + { + text: 'PriceAuthority Admin Object', + link: '/reference/zoe-api/price-authority-admin', + }, + { + text: 'MutableQuote Object', + link: '/reference/zoe-api/mutable-quote', + }, ], }, { @@ -482,6 +556,33 @@ export default defineConfig({ text: 'JavaScript Framework', ariaLabel: 'JavaScript Framework', link: '/guides/js-programming/', + collapsed: true, + items: [ + { + text: 'Hardened JavaScript', + link: '/guides/js-programming/hardened-js', + }, + { + text: 'E() for Eventual Send', + link: '/guides/js-programming/eventual-send', + }, + { + text: 'Far(), Remotables, and Marshaling', + link: '/guides/js-programming/far', + }, + { + text: 'Exo Objects and Interface Guards', + link: '/guides/js-programming/exo', + }, + { + text: 'Zones and Persistence', + link: '/guides/js-programming/zones', + }, + { + text: 'Notifiers, Subscriptions, and PublishKit', + link: '/guides/js-programming/notifiers', + }, + ], }, { @@ -494,10 +595,6 @@ export default defineConfig({ text: 'PSM Contract', link: '/guides/zoe/actual-contracts/PSM', }, - { - text: 'PriceAuthority Object', - link: '/reference/zoe-api/price-authority', - }, ], }, @@ -528,21 +625,17 @@ export default defineConfig({ items: [ { text: 'Takeaway 1: Sell Concert Tickets Contract Overview', - link: - '/guides/getting-started/sell-concert-tickets-contract-explainer', + link: '/guides/getting-started/sell-concert-tickets-contract-explainer', }, { text: 'Takeaway 2: Swaparoo Contract Overview', - link: - '/guides/getting-started/swaparoo-how-to-swap-assets-explainer', + link: '/guides/getting-started/swaparoo-how-to-swap-assets-explainer', }, { - text: - 'Takeaway 3: Sending Invitation Payments using an Address', - link: - '/guides/getting-started/swaparoo-making-a-payment-explainer', + text: 'Takeaway 3: Sending Invitation Payments using an Address', + link: '/guides/getting-started/swaparoo-making-a-payment-explainer', }, ], }, diff --git a/main/e2e-testing.md b/main/e2e-testing.md index a42a094d8..63c1c5ae9 100644 --- a/main/e2e-testing.md +++ b/main/e2e-testing.md @@ -4,13 +4,24 @@ sidebar: auto # Testing +This page covers two different kinds of end-to-end testing on Agoric: +browser-driven DApp testing with Keplr, and on-chain testing of the chain +software itself (upgrades and core-eval proposals). If you're testing a +frontend that talks to a wallet, start with +[Testing DApps with `@agoric/synpress`](#testing-dapps-with-agoric-synpress) +below. If you're testing chain software, a governance proposal, or a +contract's on-chain behavior across a chain upgrade, see +[Testing chain software with `@agoric/synthetic-chain`](#testing-chain-software-with-agoric-synthetic-chain). + +## Testing DApps with `@agoric/synpress` + Developing Decentralized Applications (DApps) on the Cosmos network often involves integrating with the Keplr wallet. Testing is key for ensuring users can flawlessly interact with your DApp through the Keplr wallet. -[`@agoric/synpress`](https://github.com/agoric-labs/synpress) is an end-to-end (e2e) testing framework that simplifies the testing process for Keplr-based DApps. This framework automates testing how your DApp interacts with the Keplr wallet, simulating real user experiences. `@agoric/synpress` is built upon [`synthetixio/synpress`](https://github.com/Synthetixio/synpress), a framework designed for Metamask-based DApps. +[`@agoric/synpress`](https://github.com/agoric-labs/synpress) is an end-to-end (e2e) testing framework that simplifies the testing process for Keplr-based DApps. This framework automates testing how your DApp interacts with the Keplr wallet, simulating real user experiences. `@agoric/synpress` is a long-lived fork of [`synthetixio/synpress`](https://github.com/synpress/synpress) predating that project's move to a Playwright-only architecture; `@agoric/synpress` still uses the original Cypress-based API described below. -Since `@agoric/synpress` is based on [Cypress](https://www.cypress.io/), the official [Cypress documentation](https://docs.cypress.io/guides/overview/why-cypress) has a lot of information that you can use with `@agoric/synpress`. +Since `@agoric/synpress` is based on [Cypress](https://www.cypress.io/), the official [Cypress documentation](https://docs.cypress.io/app/get-started/why-cypress) has a lot of information that you can use with `@agoric/synpress`. -## Installation +### Installation Before you start testing your DApp with Keplr, you'll need to install the `@agoric/synpress` in your project using the following command: @@ -18,7 +29,7 @@ Before you start testing your DApp with Keplr, you'll need to install the `@agor yarn add -D @agoric/synpress ``` -## Project structure +### Project structure We recommend the following project structure for keeping your e2e tests clean and manageable: @@ -33,7 +44,7 @@ project_dir └── test.spec.js ``` -## Configure `synpress.config.js` +### Configure `synpress.config.js` Before you start writing your E2E tests, you'll need to create a configuration file named `synpress.config.js` in the `tests/e2e` directory of your project. @@ -47,8 +58,8 @@ module.exports = defineConfig({ ...baseConfig, e2e: { ...baseConfig.e2e, - baseUrl: 'http://localhost:5173' - } + baseUrl: 'http://localhost:5173', + }, }); ``` @@ -62,7 +73,7 @@ Since you have a separate `synpress.config.js` in the `tests/e2e` folder, you ne EXTENSION=keplr synpress run --configFile=tests/e2e/synpress.config.js ``` -## Creating a Support File +### Creating a Support File Navigate to your project's `tests/e2e` directory and create a new file named `support.js`. This file can be used to create reusable commands that encapsulate common testing actions. For example, you can create a custom command to navigate to a specific page within your application. You can read more about it [over here](https://docs.cypress.io/api/cypress-api/custom-commands). @@ -74,11 +85,11 @@ import '@agoric/synpress/support/index'; This import is essential because it brings in the necessary functionalities from `@agoric/synpress` to interact with the Keplr wallet within your e2e tests. Without it, your tests won't be able to leverage the features provided by `@agoric/synpress` for Keplr integration. -## Example Tests +### Example Tests With the environment set up, let's write end-to-end (e2e) tests to test your DApp's functionality. In the `tests/e2e/specs` folder, create a new file with a descriptive name that reflects what it tests (e.g., `user_login.spec.js` or `token_transfer.spec.js`). -### Test Structure +#### Test Structure You use `describe` blocks to group related tests together, and `it` blocks to define individual test cases within those groups. @@ -90,12 +101,12 @@ describe('User Login', () => { }); ``` -### Test 1: Setting Up Keplr Wallet +#### Test 1: Setting Up Keplr Wallet ```js it('should setup a Keplr wallet', () => { cy.setupWallet({ - secretWords: 'KEPLR_MNEMONIC' + secretWords: 'KEPLR_MNEMONIC', }); cy.visit('/'); }); @@ -105,7 +116,7 @@ This test case simulates setting up a Keplr wallet for your tests, using the `cy After setting up the wallet, we visit the root path (`/`) of the DApp using `cy.visit('/')`. -### Test 2: Connecting Keplr Wallet +#### Test 2: Connecting Keplr Wallet ```js it('should accept connection with wallet', () => { @@ -116,7 +127,7 @@ it('should accept connection with wallet', () => { This test simulates a user connecting their Keplr wallet to your DApp. `cy.contains('Connect Wallet')` searches for an element containing the text `Connect Wallet` on the webpage and triggers a click event. `cy.acceptAccess` simulates accepting Keplr wallet access for your DApp. -### Test 3: Signing a Transaction +#### Test 3: Signing a Transaction ```js it('should confirm make an offer transaction', () => { @@ -127,7 +138,7 @@ it('should confirm make an offer transaction', () => { This test simulates transaction Signing on your DApp. `cy.contains('Make an Offer')` searches for an element containing the text `Make an Offer` and triggers a click event. `cy.confirmTransaction` simulates confirming a transaction. -### Running the Tests +#### Running the Tests You can now trigger the tests by running this command: @@ -135,18 +146,18 @@ You can now trigger the tests by running this command: EXTENSION=keplr synpress run --configFile=tests/e2e/synpress.config.js ``` -## Projects Using `@agoric/synpress` +### Projects Using `@agoric/synpress` Some examples projects utilizing `@agoric/synpress` for e2e tests: - [PSM dApp E2E Tests](https://github.com/Agoric/dapp-psm/tree/main/tests/e2e) - [Wallet dApp E2E Tests](https://github.com/frazarshad/wallet-app/tree/main/test/e2e) -## @agoric/synpress Commands +### @agoric/synpress Commands The `@agoric/synpress` commands are custom Cypress commands designed for use with Keplr. They provide ready-made functions to interact with Keplr-specific elements and workflows. -### `setupWallet` +#### `setupWallet` Initializes a Keplr wallet for testing. You can either create a new one or import an existing one using the provided options. @@ -167,7 +178,7 @@ The command takes a single object as an argument with the following keys: - _(**boolean**)_: `true` if the wallet was set up successfully, `false` otherwise. -### `acceptAccess` +#### `acceptAccess` Handles the authorization process for connecting the Keplr wallet to a DApp. DApps provide a `Connect Wallet` option to initiate this process. @@ -175,7 +186,7 @@ Handles the authorization process for connecting the Keplr wallet to a DApp. DAp - _(**boolean**)_: `true` if access was accepted successfully, `false` otherwise. -### `confirmTransaction` +#### `confirmTransaction` Handles signing transactions within a DApp using the Keplr wallet. This occurs when a user initiates a transaction, such as sending tokens or interacting with a smart contract. @@ -183,7 +194,7 @@ Handles signing transactions within a DApp using the Keplr wallet. This occurs w - _(**boolean**)_: `true` if transaction was approved successfully, `false` otherwise. -### `disconnectWalletFromDapp` +#### `disconnectWalletFromDapp` Disconnects the Keplr wallet from all previously connected DApps. @@ -191,7 +202,7 @@ Disconnects the Keplr wallet from all previously connected DApps. - _(**boolean**)_:`true` if the disconnection was successful, `false` otherwise. -### `getWalletAddress` +#### `getWalletAddress` Retrieves the wallet address of the currently active wallet associated with a specified blockchain. @@ -203,7 +214,7 @@ Retrieves the wallet address of the currently active wallet associated with a sp - _(**string**)_: The wallet address for the specified blockchain. -### `switchWallet` +#### `switchWallet` Switches the active wallet to the specified wallet by name. @@ -215,7 +226,7 @@ Switches the active wallet to the specified wallet by name. - _(**boolean**)_: `true` if the wallet switch was successful, `false` otherwise. -### `addNewTokensFound` +#### `addNewTokensFound` Adds all the new tokens discovered by the Keplr extension to the user's token portfolio when it connects to a new blockchain network. @@ -223,7 +234,7 @@ Adds all the new tokens discovered by the Keplr extension to the user's token po - _(**boolean**)_: `true` if the tokens were successfully added to the user's token portfolio `false` otherwise. -### `getTokenAmount` +#### `getTokenAmount` Retrieves the balance of a specified token in the wallet. @@ -235,7 +246,7 @@ Retrieves the balance of a specified token in the wallet. - _(**number**)_: The amount of the specified token currently held in the wallet. -### `isExtensionWindowActive` +#### `isExtensionWindowActive` Determines whether the Keplr extension window is the currently active tab. @@ -243,7 +254,7 @@ Determines whether the Keplr extension window is the currently active tab. - _(**boolean**)_:`true` if the Keplr extension window is active, `false` otherwise. -### `switchToExtensionWindow` +#### `switchToExtensionWindow` Switches the focus to the Keplr extension window tab. Keplr window is used for managing wallet related operations. @@ -251,7 +262,7 @@ Switches the focus to the Keplr extension window tab. Keplr window is used for m - _(**boolean**)_:`true` if the focus change was successful, `false` otherwise. -### `isCypressWindowActive` +#### `isCypressWindowActive` Determines whether the Cypress window, where the DApp is running, is the currently active tab. @@ -259,7 +270,7 @@ Determines whether the Cypress window, where the DApp is running, is the current - _(**boolean**)_:`true` if the Cypress window is active, `false` otherwise. -### `switchToCypressWindow` +#### `switchToCypressWindow` Switches the focus to the Cypress window tab. @@ -270,3 +281,59 @@ Switches the focus to the Cypress window tab.
For a full list of commands, you can refer to the [Keplr plugin file](https://github.com/agoric-labs/synpress/blob/dev/plugins/keplr-plugin.js). + +## Testing chain software with `@agoric/synthetic-chain` + +The tests above exercise a DApp frontend against a wallet. A different kind of +e2e test exercises the chain software itself: does a governance proposal or a +chain software upgrade actually produce the on-chain state it's supposed to, +against an environment that behaves like the real `agoric-3` mainnet chain? +That's what +[`a3p-integration`](https://github.com/Agoric/agoric-sdk/tree/master/a3p-integration) +in `agoric-sdk` is for, built on the +[`@agoric/synthetic-chain`](https://www.npmjs.com/package/@agoric/synthetic-chain) +test tool (published from the +[`agoric-3-proposals`](https://github.com/Agoric/agoric-3-proposals) repository). + +`a3p-integration` spins up a synthetic `agoric-3`-like chain in Docker, +applies one or more **proposals** (a chain software upgrade, a core-eval +proposal, or both in sequence), and then runs functional tests against the +result. This is the harness the SDK itself uses to verify that a chain +upgrade or a core-eval (like deploying Fast USDC) behaves as intended before +it goes to a real network. + +### Proposal packages + +Each proposal lives in its own package under `a3p-integration/proposals`, +isolated from the rest of the SDK and from each other; they run inside the +Docker image and can't reach SDK source directly. A proposal's `package.json` +`agoricProposal` field configures its type (`Software Upgrade Proposal` or +`/agoric.swingset.CoreEvalProposal`), its ordering relative to other +proposals, and, for a core-eval, the submission it evaluates. + +### Running it + +`a3p-integration` is a self-contained Yarn 4 project, not part of the +`agoric-sdk` root workspace (so it doesn't run under the root CI for +`packages/`). From `agoric-sdk/a3p-integration`, with Docker and Buildx +available: + +```bash +corepack enable +yarn install + +# build the synthetic chain image, including this checkout's chain software +yarn build + +# run every proposal's tests +yarn test + +# run a single proposal's tests, or drop into it interactively +yarn test -m fast-usdc --debug +``` + +`-m ` matches proposal package names, so `yarn test -m fast-usdc` +runs (or `--debug`-attaches to) just the Fast USDC core-eval proposal. See the +[`a3p-integration` README](https://github.com/Agoric/agoric-sdk/blob/master/a3p-integration/README.md) +and the [`agoric-3-proposals` README](https://github.com/Agoric/agoric-3-proposals/blob/main/README.md) +for the full proposal-package layout and how to add a new one. diff --git a/main/glossary/index.md b/main/glossary/index.md index 946922bb9..54aee7865 100644 --- a/main/glossary/index.md +++ b/main/glossary/index.md @@ -104,6 +104,19 @@ For more information, see the [ERTP documentation's AmountValue section](/guides [Purses](#purse) and [payments](#payment) are AssetHolders. These are objects that contain digital assets in the quantity specified by an [amount](#amount). +## Async Flow + +`@agoric/async-flow` lets an `async function` itself, not just an individual +awaited value, survive its hosting vat's upgrade. A prepared guest function's +first invocation runs it directly; on later replay (after an upgrade that +interrupted it mid-flight) the function's arguments, awaited results, and +console output are replayed from a durable log rather than re-executed, so it +picks up exactly where it left off. See the +[Async Flow guide](/guides/orchestration/async-flow) for a worked example, and +[Vows and Durable Eventual-Send](/guides/js-programming/eventual-send#vows-and-durable-eventual-send), +which async-flow is built on, and the +[async-flow README](https://github.com/Agoric/agoric-sdk/tree/master/packages/async-flow). + ## Baggage `baggage` is a `MapStore` that provides a way to preserve state and behavior of objects between [smart contract upgrades](/guides/zoe/contract-upgrade) in a way that preserves identity of objects as seen from other [vats](#vat). @@ -143,7 +156,7 @@ Before a contract can be installed on Zoe, its source code must be bundled. This ```js import bundleSource from '@endo/bundle-source'; const atomicSwapBundle = await bundleSource( - require.resolve('@agoric/zoe/src/contracts/atomicSwap') + require.resolve('@agoric/zoe/src/contracts/atomicSwap'), ); ``` @@ -204,6 +217,20 @@ in which each key is a string and each value is [passable](#passable), such as For more information, see the [Marshaling section in the JavaScript Distributed Programming Guide](/guides/js-programming/far#marshaling-by-copy-or-by-presence). +## Crank + +A crank is the kernel's transactional unit of work: one message delivery or promise-resolution +notification given to a single vat. Whatever state changes the vat makes during a crank, and +whatever messages it sends out, commit or roll back together with it; a +[message embargo](#message-embargo) holds those outbound messages back until the crank has been +durably committed, which is what keeps a [hangover inconsistency](#hangover-inconsistency) from +ever reaching the outside world. A contract's first crank must define all its [Exo](#exo) classes +and kits before handling any incoming method call from another vat; see +[Crank in the Contract Upgrade guide](/guides/zoe/contract-upgrade#crank). + +For more information, see +[Cranks in the SwingSet run-policy docs](https://github.com/Agoric/agoric-sdk/blob/master/packages/SwingSet/docs/run-policy.md#cranks). + ## Creator Invitation An [invitation](#invitation) optionally returned by [`E(zoe).startInstance(...)`](/reference/zoe-api/zoe#e-zoe-startinstance-installation-issuerkeywordrecord-terms-privateargs) that the contract instance @@ -220,6 +247,17 @@ appropriately branded assets to the purse, but cannot withdraw assets from the p Dynamic version of the [Inter-Blockchain Communication](#ibc) protocol. For more details, see the [agoric-sdk `network` package](https://github.com/Agoric/agoric-sdk/tree/master/packages/network). +## Durable + +A [Zone](#zone)'s durable regime: [Exo](#exo) instances and stores allocated +in a durable zone survive not just paging to disk (as [virtual](#virtual) +instances do) but also a [vat](#vat) upgrade, becoming addressable again from +[baggage](#baggage) in the successor incarnation. Durable state is the +mechanism behind [Orthogonal Persistence](#orthogonal-persistence) at the +application level. See +[Zones and Persistence](/guides/js-programming/zones) for the full +heap/virtual/durable picture. + ## E() (Also referred to as _eventual send_) `E()` is a local "bridge" function that @@ -290,6 +328,10 @@ This [`@endo/exo` package](https://github.com/endojs/endo/tree/master/packages/e the APIs for making Exo objects, and for defining ExoClasses and ExoClassKits for making Exo objects. +See [Exo Objects and Interface Guards](/guides/js-programming/exo) for a full +guide, including [InterfaceGuard](#interfaceguard)s and the heap/virtual/durable +[Zone](#zone) regimes an Exo class can be allocated in. + ## Facet A _facet_ is an object that exposes an API or particular view of some larger entity, which may be an object itself. @@ -298,7 +340,7 @@ You can make any number of facets of an entity. In JavaScript, you often make a ```js import { Far } from '@endo/far'; const facet = Far('FacetName', { - myMethod: (...args) => oldObject.method(...args) + myMethod: (...args) => oldObject.method(...args), }); ``` @@ -321,12 +363,34 @@ is meaningful, so handles do not have properties. Unlike number or string identi handles are unforgeable. This means the only way to know a handle identity is being given an object reference, and no identity can be guessed and no fake identity will succeed. +## Hangover Inconsistency + +The failure mode where a system has durably committed to having told the outside world +something (sent a message, released a payment) that its own, truer state then contradicts, +typically because a crash or reboot rewound it to an earlier checkpoint after the outbound +message had already gone out. SwingSet avoids it, following the lead of the Waterken and E +systems, with a [message embargo](#message-embargo): output is held back until the +[crank](#crank) that produced it has been durably committed, so a rewind can never leave a +stale message already delivered. + +For more information, see +[the SwingSet host-app docs](https://github.com/Agoric/agoric-sdk/blob/master/packages/SwingSet/docs/host-app.md). + ## Harden A hardened object’s properties cannot be changed, so the only way to interact with a hardened object is through its methods. `harden()` is similar to `Object.freeze()` but more powerful. For more about `harden()`, see its [section in the JavaScript Distributed Programming Guide](https://github.com/endojs/endo/blob/HEAD/packages/ses/docs/guide.md) +## Heap + +A [Zone](#zone)'s default regime: [Exo](#exo) instances and stores allocated +in a heap zone live as ordinary JavaScript objects, occupying room in the +vat's heap and its snapshot, and are lost when the vat terminates (including +a normal upgrade). Appropriate for low-cardinality, short-lived state; see +[virtual](#virtual) and [durable](#durable) for the alternatives, and +[Zones and Persistence](/guides/js-programming/zones) for the full picture. + ## Hardened JavaScript (SES) Hardened JavaScript is a standards-track extension to the JavaScript standard. @@ -345,6 +409,18 @@ For more details, see [What developers need to know about inter-blockchain commu Interchain Accounts are an [IBC](#ibc) feature used in Agoric's [Orchestration API](#orchestration) to enable an Agoric smart contract to control an account on another blockchain within the Cosmos ecosystem. This feature leverages the [Inter-Blockchain Communication (IBC)](#ibc) protocol to facilitate interactions and transactions across different blockchains seamlessly. +## InterfaceGuard + +An InterfaceGuard, built with `M.interface(...)` from +[`@endo/patterns`](https://github.com/endojs/endo/tree/master/packages/patterns), +declares the argument and return shape of each method on an [Exo](#exo) +object, so that calls are validated automatically at the boundary before the +method body runs. `M.call(...)` describes a synchronous method, +`M.callWhen(...)` describes an async method whose promise or +[Vow](#vow) arguments should be awaited and validated before the resolved +values reach the method. See +[Exo Objects and Interface Guards](/guides/js-programming/exo). + ## Invitation A [payment](#payment) whose amount represents (and is required for) participation in a contract instance. @@ -404,6 +480,19 @@ A _Keyword_ is a string that is an ASCII-only [identifier](https://developer.moz starts with an upper case letter, and is not equal to "NaN" or "Infinity". See **[Zoe Data Types](/reference/zoe-api/zoe-data-types#keyword)**. +## Message Embargo + +The discipline of holding all outbound communication (device output, messages to other +machines) until every consequence of the inbound delivery that produced it has been durably +committed to storage. Embargoed output is exactly what protects against +[hangover inconsistency](#hangover-inconsistency): if the [crank](#crank) is rolled back before +commit, the embargoed messages are simply discarded rather than having to be un-sent. + +For more information, see +[the SwingSet host-app docs](https://github.com/Agoric/agoric-sdk/blob/master/packages/SwingSet/docs/host-app.md) +and +[the VatTP mailbox device docs](https://github.com/Agoric/agoric-sdk/blob/master/packages/SwingSet/docs/devices.md). + ## Mint [ERTP](#ertp) has a _mint_ object, which creates digital assets. [ZCF](#zcf) provides a different interface to an ERTP mint, called a @@ -563,7 +652,7 @@ For example: const myProposal = harden({ give: { Asset: AmountMath.make(quatloosBrand, 4n) }, want: { Price: AmountMath.make(moolaBrand, 15n) }, - exit: { onDemand: null } + exit: { onDemand: null }, }); ``` @@ -571,6 +660,17 @@ const myProposal = harden({ See [Offers](/guides/zoe/proposal). +## PublishKit + +The current recommended pub-sub primitive, made with `makePublishKit()` from +[`@agoric/notifier`](https://github.com/Agoric/agoric-sdk/tree/master/packages/notifier), +returning a `{ publisher, subscriber }` pair. Unlike the deprecated +`NotifierKit` (always lossy) and `SubscriptionKit` (always lossless), a +single `subscriber` supports both `subscribeEach` (forward-lossless) and +`subscribeLatest` (lossy) consumption. A **Durable PublishKit**, made with +`prepareDurablePublishKit(baggage, kindName)`, additionally survives a vat +upgrade. See [Notifiers, Subscriptions, and PublishKit](/guides/js-programming/notifiers). + ## Purse A purse holds [amounts](#amount) of assets issued by a particular [mint](#mint) that are all of the same [brand](#brand), often for arbitrarily long periods of time. @@ -589,6 +689,17 @@ episode [The Gamesters of Triskelion](https://en.wikipedia.org/wiki/The_Gamester A transfer of [amounts](#amount) between [seats](#seat) within Zoe; i.e. a change in their [allocations](#allocation). When a seat exits, it gets its current allocation as a [payout](#payout). +## Remotable + +An object created by [`Far()`](#far) or an [Exo](#exo)-making function +(`makeExo`, `defineExoClass`, `defineExoClassKit`, or their virtual/durable +counterparts). A Remotable is [passable](#passable): it crosses a +[marshal](/guides/js-programming/far#marshaling-by-copy-or-by-presence) +boundary as a capability reference (a [presence](#presence) on the far side) +rather than being copied, and any vat holding a reference to one can invoke +its methods with [`E()`](#e). `getInterfaceOf(x)` returns a Remotable's +interface name; `passStyleOf(x)` reports `"remotable"` for one. + ## Seat Zoe uses a seat to represent an [offer](#offer) in progress, and has two seat [facets](#facet) representing @@ -620,6 +731,18 @@ Secure ECMAScript has been renamed to [Hardened JavaScript](#hardened-javascript An imaginary currency Agoric documentation uses in examples. +## Smallcaps + +The current [marshal](https://github.com/endojs/endo/tree/master/packages/marshal) +wire encoding for [Passable](#passable) data, superseding the older `capdata` +`@qclass`-tagged encoding. Smallcaps represents non-JSON-native values +(`BigInt`s, `undefined`, [Remotables](#remotable), promises, symbols) as +specially prefixed strings (for example, a leading `$` marks a remotable +slot reference, `+`/`-` mark a bigint) so the overall structure remains +plain, round-trippable JSON. Application code never encodes or decodes +smallcaps directly; it's produced and consumed by `@endo/marshal`'s +`toCapData`/`fromCapData` on your behalf. + ## Smart Contract A smart contract is a contract-​like arrangement expressed in code, where the behavior of the @@ -639,6 +762,20 @@ it set at $10. They can specify the instance's minimum bid amount in its terms. For example, see [starting the offer-up contract](/guides/zoe/contract-walkthru.html#starting-a-contract-instance). +## Turn + +A turn is the synchronous processing of one pending delivery: a return to an empty call stack. +A [vat](#vat)'s state evolves only through a sequence of turns, each running to completion with +exclusive access to that state before the next one begins, so a turn is atomic and +indivisible from every other vat's point of view. A [crank](#crank) is the coarser-grained +unit built from turns: the kernel's return to an empty event queue, encompassing however many +turns the vat needs to settle all the synchronous and locally-queued work that one delivery +provokes, before the crank as a whole can commit. + +For more information, see +[Vats: the Unit of Synchrony](/guides/js-programming/#vats-the-unit-of-synchrony) and +["Turns as Micro-Transactions"](https://erights.github.io/erights-org-website/elib/concurrency/turns.html). + ## Vat A vat is a unit of isolation. @@ -649,9 +786,42 @@ For more information, see the [Vat section in the Distributed JS Programming doc The concept of a vat is metaphorically inspired by the philosophical experiment known as “Brain in a Vat.” This thought experiment, explored in philosophy, posits a scenario in which a brain is sustained in a vat and connected to a computer that simulates reality, raising questions about knowledge, reality, and perception. Similarly, in the context of distributed systems, a vat isolates and encapsulates its contents, maintaining strict boundaries on synchronous and asynchronous communication, much like the hypothetical brain’s isolated and controlled environment. For more details, see the Wikipedia page on [Brain in a Vat](https://en.wikipedia.org/wiki/Brain_in_a_vat). +## Virtual + +A [Zone](#zone)'s virtual regime: like [heap](#heap), [Exo](#exo) instances +and stores allocated in a virtual zone are lost when the vat terminates, but +unlike heap allocation, they're paged out to external storage rather than +kept in the JS heap, so a vat can hold far more virtual instances at once +than would fit in memory (high cardinality). Virtual addresses memory +pressure only; for state that must also survive a vat upgrade, use +[durable](#durable). + ## Vow -Vows are objects that represent promises that can be stored durably. Native promises are not compatible with Agoric's durable stores, which means that on the Agoric platform, such promises disconnect their clients when their creator vat is upgraded. +Vows are objects (from +[`@agoric/vow`](https://github.com/Agoric/agoric-sdk/tree/master/packages/vow)) +that represent promises that can be stored durably. Native promises are not +compatible with Agoric's durable stores, which means that on the Agoric +platform, such promises disconnect their clients when their creator vat is +upgraded. + +`prepareVowTools(zone)` returns the operators for working with Vows, bound to +a particular [zone](#zone)'s storage: + +- **`when(vowOrPromise)`** returns a promise for a Vow's eventual settlement, + retrying automatically across a disconnection caused by an upgrade of some + other vat, though it cannot itself survive an upgrade of its own calling + vat. +- **`watch(vowOrPromise, watcher, context)`** subscribes a durable watcher to + a Vow such that the subscription survives an upgrade of both the producing + and the consuming vat, since the watcher and its resumption state live in + baggage rather than a closure. +- **`watchPromise(promise, watcher, ...args)`** (from `@agoric/base-zone`) is + the equivalent durable subscription for an ordinary, non-Vow promise. + +See [Vows and Durable Eventual-Send](/guides/js-programming/eventual-send#vows-and-durable-eventual-send) +for the full picture, and [Async Flow](#async-flow) for the companion +mechanism covering entire `async function` bodies. ## Wallet @@ -683,6 +853,7 @@ A set of API methods for deploying and working with smart contracts. See [Zoe Se ## Zone -Each Zone provides an API that allows the allocation of [Exo objects](#exo) and [Stores (object collections)](https://github.com/Agoric/agoric-sdk/tree/master/packages/store/README.md) which use the same underlying persistence mechanism. This allows library code to be agnostic to whether its objects are backed purely by the JS heap (ephemeral), pageable out to disk (virtual) or can be revived after a vat upgrade (durable). +Each Zone provides an API that allows the allocation of [Exo objects](#exo) and [Stores (object collections)](https://github.com/Agoric/agoric-sdk/blob/master/packages/store/README.md) which use the same underlying persistence mechanism. This allows library code to be agnostic to whether its objects are backed purely by the JS heap ([heap](#heap)), pageable out to disk ([virtual](#virtual)) or can be revived after a vat upgrade ([durable](#durable)). -See [SwingSet vat upgrade documentation](https://github.com/Agoric/agoric-sdk/tree/master/packages/SwingSet/docs/vat-upgrade.md) for more example use of the zone API. +See [Zones and Persistence](/guides/js-programming/zones) for a full guide, and the +[SwingSet vat upgrade documentation](https://github.com/Agoric/agoric-sdk/blob/master/packages/SwingSet/docs/vat-upgrade.md) for more example use of the zone API. diff --git a/main/guides/UIComponentLibrary/assets/asset.md b/main/guides/UIComponentLibrary/assets/asset.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/main/guides/UIComponentLibrary/index.md b/main/guides/UIComponentLibrary/index.md index bedb02f00..b7d9570e0 100644 --- a/main/guides/UIComponentLibrary/index.md +++ b/main/guides/UIComponentLibrary/index.md @@ -1,28 +1,68 @@ # Welcome To Agoric's UI Component Library -You'll find a collection of ready-to-use or pre-coded user interface elements that help accelerate your Agoric Dapp-building. These pre-designed elements include wallet connect buttons, chain selector, node selector, amount input, smart wallet provisioning and leap elements. -Utilization of these building blocks ensures best practices in design and accessibility. +[`@agoric/react-components`](https://github.com/Agoric/ui-kit/tree/main/packages/react-components), +part of the [`Agoric/ui-kit`](https://github.com/Agoric/ui-kit) monorepo, provides React hooks and a +small set of pre-built components (a wallet-connect button, an amount input, +and a network-notice banner) that handle the repetitive parts of an Agoric +dapp frontend: watching vstorage, connecting to Keplr, and reading/writing +smart-wallet state. Utilization of these building blocks ensures best +practices in design and accessibility. + +::: tip +The component set here has changed over time; some previously documented +components (a dedicated chain selector, node selector, and "Leap Elements") +are no longer part of the package. What's below reflects the current +`react-components` source. +::: ## Installation and Setup -See [Introduction to Agoric UI Kit](https://a0a31cba.documentation-7tp.pages.dev/guides/getting-started/how-to-use-ui-kit#introduction-to-agoric-ui-kit) for installation and setup how-to. - -Once complete you'll need to run this yarn command: +See [`Agoric/ui-kit` README - Setup](https://github.com/Agoric/ui-kit#setup) +for setup instructions (these packages require [SES](https://github.com/endojs/endo/tree/master/packages/ses), +so lockdown must run before your app's other code), and +[`packages/react-components`](https://github.com/Agoric/ui-kit/tree/main/packages/react-components) +for the package overview. ```sh yarn add @agoric/react-components ``` +Most dapps also want [`@agoric/rpc`](https://github.com/Agoric/ui-kit/tree/main/packages/rpc) +(for `makeAgoricChainStorageWatcher`, to read vstorage) and +[`@agoric/web-components`](https://github.com/Agoric/ui-kit/tree/main/packages/web-components) +(for `makeAgoricWalletConnection`, to connect to Keplr and make/exit offers); +`react-components` wires both of these together behind the `useAgoric` hook +described below. + ## UI Components List -These React UI components can be immediately used to streamline your Agoric Dapp UI +These React UI components can be immediately used to streamline your Agoric Dapp UI: - Connect Wallet -- Wallet Provisioning - Amount Input -- Chain Selector -- Node Selector -- Leap Elements +- Notice Banner + +## The `useAgoric` hook and `AgoricContext` + +Most Agoric-related state (wallet address, connected chain, purses, +whether the smart wallet is provisioned, and helpers to make and exit offers) +is accessible through the `useAgoric()` hook, backed by `AgoricContext`. See +[`AgoricContext`](https://github.com/Agoric/ui-kit/blob/main/packages/react-components/src/lib/context/AgoricContext.ts) +for the full interface, which includes: + +| Field | Type | Description | +| --------------------------------------------------------- | ------------------------------- | ----------------------------------------------------- | +| `address` | `string \| undefined` | The connected wallet's address | +| `purses` | `PurseJSONState[] \| undefined` | The connected wallet's purses | +| `isSmartWalletProvisioned` | `boolean \| undefined` | Whether the address has a provisioned smart wallet | +| `provisionSmartWallet` | function | Provisions a smart wallet for the connected address | +| `smartWalletProvisionFee` / `smartWalletProvisionFeeUnit` | `bigint` / `string` | The current provisioning fee, and its denom | +| `makeOffer` / `exitOffer` | function | Make or exit a Zoe offer through the connected wallet | + +There is no separate "Smart Wallet Provisioning" component; a dapp reads +`isSmartWalletProvisioned` and `smartWalletProvisionFee` from `useAgoric()` +and calls `provisionSmartWallet` from its own UI (for example, from a +[`NoticeBanner`](#notice-banner)). ## Connect Wallet @@ -30,94 +70,49 @@ These React UI components can be immediately used to streamline your Agoric Dapp alt="Screenshot: Connect Wallet" src="./assets/connectwallet.png" /> -Screenshot: Wallet Address - -Screenshot: Select Wallet +`ConnectWalletButton` shows "Connect Wallet" until a wallet is connected, then +shows the truncated connected address. -- [Connecting with Wallet Connect Button](https://github.com/Agoric/ui-kit/tree/main/packages/react-components#integrating) -- [Connecting without Wallet Connect Button](https://github.com/Agoric/ui-kit/tree/main/packages/react-components#connecting-without-connectwalletbutton) -- [Using a Custom Chain Provider](https://github.com/Agoric/ui-kit/tree/main/packages/react-components#using-a-custom-chainprovider) +- [`ConnectWalletButton` source](https://github.com/Agoric/ui-kit/blob/main/packages/react-components/src/lib/components/ConnectWalletButton.tsx) Customizable Parameters | Parameter | Type | Description | |----------|----------|----------| -| ClassName | string | CSS class name for the underlying `