diff --git a/website/docs/getting-started/my-first-diamond.mdx b/website/docs/getting-started/my-first-diamond.mdx new file mode 100644 index 00000000..23d7884a --- /dev/null +++ b/website/docs/getting-started/my-first-diamond.mdx @@ -0,0 +1,313 @@ +--- +sidebar_position: 2 +title: "Building Your First Diamond" +sidebar_label: "Build Your First Diamond" +description: "Follow along to scaffold, build, test, and deploy your first diamond smart contract application with Compose" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import DocCard, { DocCardGrid } from '@site/src/components/docs/DocCard'; +import Callout from '@site/src/components/ui/Callout'; +import ExpandableCode from '@site/src/components/code/ExpandableCode'; + +# Building Your First Diamond + + +Create a working diamond project using Compose then explore every file so you know exactly what you have. + + +This tutorial uses the `Counter` example: a minimal diamond with increment and read functions. It's the simplest way to see how diamonds works end-to-end. + +## Prerequisites + +- [Node.js](https://nodejs.org/) >= 20 +- [Foundry](https://book.getfoundry.sh/getting-started/installation) installed and on your PATH + + +Althought the CLI support both Foundry and Hardhat, this tutorial will focus on a Foundry based project. All learnings from the diamond archtiecture are the same for both framework + +If you haven't used Foundry before, follow the official installation guide first. You only need `forge` and `anvil` for this tutorial. + + +## Step 1) Install the CLI + +Install the Compose CLI so you can run all the CLI commands + + +{`npm install -g @perfect-abstractions/compose-cli`} + + +Verify it's installed: + + +{`compose --help`} + + +You can skip the global package installation. Just use `npx @perfect-abstractions/compose-cli` wherever you see `compose` in this tutorial. + +## Step 2) Scaffold the project + + +{`compose init`} + + +You'll be prompt to choose from a lot of different options from framework, ERC standards templates, and others. + +For this tutorial, we will choose the `Counter` example with the following options: + + +{`Enter project name: my-diamond +Select project framework: Foundry +Select base: Counter +Select extension facets: None +Select Compose library facets: None +Select ownership: None +Select access control: None + +? Install project dependencies? (Y/n) Yes`} + + +Take the time to review the full library available to you. We support most major token standards like `ERC20`, `ERC721` with different type of access control. + +You can always skip the interactive prompts entirely: + +{`compose init my-first-diamond --framework foundry --base counter --yes`} + + + +When the CLI finishes, follow the next steps instruction to move into your project and test it: + + +{`✔ Project "my-first-diamond" scaffolded in ".../my-first-diamond" + +Next steps: +1. cd my-first-diamond +2. forge build && forge test`} + + +## Step 3) Explore the project structure + +The scaffold generates a diamond with two local facets and one from the Compose library. + + +{`src/ + Diamond.sol ← proxy: wires everything together + facets/ + CounterDataFacet.sol ← get the current count + CounterIncrementFacet.sol ← increment the counter`} + + +Each facet owns a slice of the logic. `CounterDataFacet` exposes the count, `CounterIncrementFacet` mutates it. The `Diamond.sol` proxy routes calls to the right facet via function selectors. + +### Diamond.sol: The proxy + +The diamond is the application entrypoint. Users call it, but it holds no logic itself, only the storage state. Every call is delegated to a stateless facet via the fallback function: + + +{`contract Diamond { + constructor(address[] memory _facets) { + // Add all the facets address provided to constructor + DiamondMod.addFacets(_facets); + } + + fallback() external payable { + DiamondMod.diamondFallback(); + } + + receive() external payable {} +}`} + + +When a function is called on the diamond, [`DiamondMod.diamondFallback()`](/docs/library/diamond/DiamondMod#diamondfallback) looks up which contract owns that selector and forwards the call. + +There nothing more to make a diamond proxy work. Now, let's transition where our application logic lives + +### CounterDataFacet.sol + +Facets need to share state. They do this by pointing to the same storage slot: + + +{`contract CounterDataFacet { + // 1. Slot address: a deterministic location shared across the diamond + bytes32 constant STORAGE_POSITION = keccak256("counter"); + + // 2. Data struct: defines the shape of data stored at that slot: + struct CounterStorage { + uint256 count; + } + + // 3. Assembly storage getter: \`storage\` reference pointer to that slot: + function getStorage() internal pure returns (CounterStorage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + // ---------------------------------- + // Facet functions + // ---------------------------------- + + function getCount() external view returns (uint256) { + return getStorage().count; + } + + // Declares which selectors this facet registers on the diamond proxy + function exportSelectors() external pure returns (bytes memory) { + return bytes.concat(this.getCount.selector); + } +}`} + + +Any facet that need `Counter` data uses the same `STORAGE_POSITION [ keccak256("counter") ]` and `CounterStorage` struct. They never import each other. They just agree on the same storage location. + +Facets can be scoped based on your project needs. Compose encourages small scoped facets, one facet per responsibility, to allow granular composition. In this example, `CounterDataFacet` handles reads and `CounterIncrementFacet` handles mutations. You could combine them into a single facet, but keeping them separate lets you pick exactly the functionality you need at anytime + +## Step 4) Build and test + +Now let's compile and run the tests: + + +{`forge build && forge test -vv`} + + +You should see compilation succeed and one passing test in `/test/Diamond.t.sol` + +The test deploys a diamond with all three facets the `Counter` needs, then calls `facetAddresses()` on `DiamondInspectFacet` to verify the right number of facets are registered. + +## Step 5) Write your own test + +Let's interact with the `Counter` through a test. Open `test/Diamond.t.sol` and take a look at what's already there: + + +{`// SPDX-License-Identifier: MIT +pragma solidity ^0.8.30; + +import {Test} from "forge-std/Test.sol"; +import {Diamond} from "../src/Diamond.sol"; +import {CounterDataFacet} from "../src/facets/CounterDataFacet.sol"; +import {CounterIncrementFacet} from "../src/facets/CounterIncrementFacet.sol"; +import {DiamondInspectFacet} from "@perfect-abstractions/compose/diamond/DiamondInspectFacet.sol"; + +contract DiamondTest is Test { + Diamond diamond; + + // This setup function runs before each test + function setUp() public { + address[] memory facets = new address[](3); + + // 1. Deploying the Counter specific facets + facets[0] = address(new CounterDataFacet()); + facets[1] = address(new CounterIncrementFacet()); + + // 2. Deploying the required Inspection Facet from @perfect-abstractions/compose + facets[2] = address(new DiamondInspectFacet()); + + // 3. Deploying your proxy with all 3 addresses passed to the constructor + diamond = new Diamond(facets); + } + + function test_inspect_facetAddresses() public view { + DiamondInspectFacet inspect = DiamondInspectFacet(address(diamond)); + address[] memory addresses = inspect.facetAddresses(); + assertEq(addresses.length, 3); + } +}`} + + +The test file already has a `setUp()` function that deploys a fresh `Diamond` instance before each test. There's also one test that verifies all three facets are properly registered on the proxy. + +Take a look at the order here. The facets are deployed first as separate contracts, then the Diamond proxy is created with references to those pre-deployed facet addresses. This is the core of [composition](/docs/design/design-for-composition) over inheritance. Instead of a monolithic contract that `is` everything through inheritance chains, the Diamond `has` the facets it needs. They're composed at runtime via the constructor or the upgrade functionality. + +Now let's add a test that actually uses our `Counter`. Add this function inside the `DiamondTest` contract: + + +{`function test_counter_incrementAndRead() public { + // Unique address entrypoint + address diamondAddress = address(diamond); + + // Cast diamond to both facet interfaces + CounterIncrementFacet incrementer = CounterIncrementFacet(diamondAddress); + CounterDataFacet getter = CounterDataFacet(diamondAddress); + + // Initial count should be 0 + assertEq(getter.getCount(), 0); + + // Increment through one facet + incrementer.increment(); + + // Counter should now be 1 + assertEq(getter.getCount(), 1); + + // Increment again by 2 (counter should now be 3) + incrementer.incrementBy(2); + assertEq(getter.getCount(), 3); +} +`} + + +Run the test suite: + + +{`forge test`} + + +All tests pass, demonstrating that data written by one facet is immediately visible to another through the diamond's shared storage. + +## Step 6) Deploy locally + +Start a local Ethereum node in one terminal: + + +{`anvil`} + + +In a second terminal, deploy your Counter application: + + +{`forge script script/Deploy.s.sol:DeployScript \\ + --rpc-url http://localhost:8545 \\ + --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \\ + --broadcast`} + + + +The private key above is Anvil's default test key. **Never use it on a real network.** + + +You'll see the deployed addresses logged: + + +{`== Logs == + CounterDataFacet: 0x5FbDB2315678afecb367f032d93F642f64180aa3 + CounterIncrementFacet: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 + DiamondInspectFacet: 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0 + Diamond: 0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9`} + + +#### Congratulations! + +You just deployed your first diamond proxy application. You've scaffolded a `Counter` project using the CLI, written tests that verify shared storage across facets, and deployed a composed system to a local network. + +We recommend you to explore all the template available to scaffold. Use the the `catalog` command to review what's available to you. + +## What's next? + + + + + + + +Run `npx @perfect-abstractions/compose-cli init` again and pick **ERC-20** or **ERC-721** to see how the same pattern scales to token standards. + diff --git a/website/src/css/code-blocks.css b/website/src/css/code-blocks.css index ae6b8885..c8adc663 100644 --- a/website/src/css/code-blocks.css +++ b/website/src/css/code-blocks.css @@ -144,7 +144,11 @@ code { } .token.comment { - color: #546e7a; - font-style: italic; + color: #425c68 !important; +} + +/* Dark mode - lighter comment color for readability on dark background */ +[data-theme='dark'] .token.comment { + color: #b0b9c7 !important; } diff --git a/website/src/css/sidebar.css b/website/src/css/sidebar.css index fd674079..c7574bda 100644 --- a/website/src/css/sidebar.css +++ b/website/src/css/sidebar.css @@ -21,12 +21,12 @@ /* Dark mode sidebar */ [data-theme='dark'] .theme-doc-sidebar-container { - background-color: #0f172a; - border-right: 1px solid #1e293b; + background-color: #020613; + border-right: 1px solid #020613; } [data-theme='dark'] .menu { - background-color: #0f172a; + background-color: #020613; } [data-theme='dark'] .theme-doc-toc-desktop { diff --git a/website/src/css/variables.css b/website/src/css/variables.css index 1d64ac53..8d9343fe 100644 --- a/website/src/css/variables.css +++ b/website/src/css/variables.css @@ -110,9 +110,9 @@ --ifm-color-accent-light: #7dd3fc; /* Dark blue backgrounds */ - --ifm-background-color: #0f172a; - --ifm-background-surface-color: #1e293b; - --ifm-navbar-background-color: #0a0e1a; + --ifm-background-color: #020613; + --ifm-background-surface-color: #020613; + --ifm-navbar-background-color: #050b1d; /* Sidebar colors */ --ifm-menu-color: #a0a0a0; @@ -131,30 +131,30 @@ --ifm-color-emphasis-900: #ffffff; /* Border colors - blue tones */ - --ifm-color-emphasis-100: #1e293b; - --ifm-color-emphasis-200: #334155; + --ifm-color-emphasis-100: #020613; + --ifm-color-emphasis-200: #020613; - --ifm-code-background: #1e293b; + --ifm-code-background: #020613; --docusaurus-highlighted-code-line-bg: rgba(59, 130, 246, 0.15); - /* Brand tokens (dark) */ - --compose-bg-900: #0f172a; - --compose-bg-800: #1e293b; +/* Brand tokens (dark) */ + --compose-bg-900: #020613; + --compose-bg-800: #020613; --compose-primary-500: #60a5fa; --compose-primary-600: #3b82f6; /* Homepage CTA + stats — reversed gradients meet at same hue (mirrors light band) */ - --home-band-bg: linear-gradient(180deg, var(--compose-bg-900) 0%, #0c1323 100%); - /* CTA bottom (#0c1323) must equal stats top — same as light reverse-bg, not duplicate of --home-band-bg */ - --home-band-reverse-bg: linear-gradient(180deg, #0c1323 0%, var(--compose-bg-900) 100%); + --home-band-bg: linear-gradient(180deg, var(--compose-bg-900) 0%, #020613 100%); + /* CTA bottom (#020613) must equal stats top — same as light reverse-bg, not duplicate of --home-band-bg */ + --home-band-reverse-bg: linear-gradient(180deg, #020613 0%, var(--compose-bg-900) 100%); /* Hero gradient - dark blue */ --hero-gradient: linear-gradient(135deg, var(--compose-bg-900) 0%, var(--compose-bg-800) 100%); - --hero-gradient-alt: linear-gradient(135deg, var(--compose-bg-800) 0%, #334155 100%); + --hero-gradient-alt: linear-gradient(135deg, var(--compose-bg-800) 0%, #020613 100%); /* Homepage hero tokens (dark overrides if needed) */ - --hero-bg-start: #0f172a; - --hero-bg-end: #1e293b; + --hero-bg-start: #020613; + --hero-bg-end: #020613; --hero-text-strong: rgba(255, 255, 255, 1); --hero-text-medium: rgba(255, 255, 255, 0.9); --hero-text-weak: rgba(255, 255, 255, 0.75); @@ -182,15 +182,15 @@ html { /* Main content area dark styling */ [data-theme='dark'] .main-wrapper { - background: #0f172a; + background: #020613; } [data-theme='dark'] .docMainContainer { - background: #0f172a; + background: #020613; } [data-theme='dark'] article { - background: #0f172a; + background: #020613; } /* Documentation content wrapper */