Created
November 12, 2025 17:16
-
-
Save crazyrabbitLTC/75a7d45a15ae473d239cd771e2ac6e45 to your computer and use it in GitHub Desktop.
Canton Network ICO Token Project LLMs.txt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # ICO Token Project – Full Rebuild Guide for LLMs | |
| This document consolidates every learning, instruction, code file, and operational step collected while building the `<ICO_PROJECT_ROOT>` project. Follow it sequentially to recreate the project from scratch, rerun the pipeline, and understand the rationale behind each decision. | |
| Throughout this guide, use the following placeholders to map commands to your local setup: | |
| - `<ICO_PROJECT_ROOT>`: directory that stores the ICO token project files (Ico.daml, Token.daml, scripts, etc.). | |
| - `<QUICKSTART_HOME>`: Canton quickstart workspace directory that contains the `quickstart` build system. | |
| - `cn-quickstart`: parent directory of `<QUICKSTART_HOME>` (used when cloning or referencing the quickstart repository). | |
| --- | |
| ## 1. Goal & Scope | |
| - Build a privacy-preserving ICO (Initial Coin Offering) on Canton Network using Daml. | |
| - Support atomic token-for-token swaps with multi-party authorization. | |
| - Provide scripts to set up dependencies, build contracts, run tests, and deploy to Canton LocalNet via cn-quickstart. | |
| - Capture all lessons learned (syntax, build, test, deployment) for future operators. | |
| --- | |
| ## 2. Repository & Directory Layout | |
| ``` | |
| <ICO_PROJECT_ROOT>/ | |
| ├── Ico.daml # ICO contract logic | |
| ├── Token.daml # Reusable fungible token module | |
| ├── IcoTest.daml # Lifecycle test (multi-party flow) | |
| ├── daml.yaml # Package configuration | |
| └── scripts/ | |
| ├── setup.sh # Verifies prerequisites and prepares local workspace | |
| ├── build.sh # Compiles contracts via the Canton quickstart build system | |
| ├── test.sh # Runs the Daml test suite | |
| └── deploy.sh # Starts LocalNet services and surfaces endpoints | |
| ``` | |
| --- | |
| ## 3. Prerequisites | |
| | Tool | Version | Notes | | |
| |------|---------|-------| | |
| | macOS/Linux/Windows | Latest | Build run on macOS Darwin 24.1.0 | | |
| | Docker Desktop | ≥ 27.0 | Required for Canton LocalNet | | |
| | tmux | Latest | All long-running commands run in tmux | | |
| | Git | Latest | Needed to clone cn-quickstart | | |
| | Java (JDK) | **21** | Install via `brew install openjdk@21` | | |
| | Daml SDK | `3.3.0-snapshot.20250502.13767.0.v2fc6c7e2` | Install with `daml install <version>` | | |
| | PATH setup | `export JAVA_HOME=/opt/homebrew/opt/openjdk@21`<br>`export PATH="$JAVA_HOME/bin:$PATH:$HOME/.daml/bin"` | Required before builds | | |
| **Key Learning:** Java can be installed but still unusable if `JAVA_HOME` and `PATH` are not configured. The build script auto-detects Java 21 at `/opt/homebrew/opt/openjdk@21`, but set the environment explicitly in shells and CI. | |
| --- | |
| ## 4. Environment Setup | |
| 1. **Clone and configure cn-quickstart** | |
| ```bash | |
| git clone https://github.com/digital-asset/cn-quickstart.git | |
| cd <QUICKSTART_HOME> | |
| make setup | |
| ``` | |
| 2. **Verify Docker** – `docker info` must succeed. | |
| 3. **Run project setup script** (automates steps above if rerun): | |
| ```bash | |
| cd <ICO_PROJECT_ROOT> | |
| ./scripts/setup.sh | |
| ``` | |
| - Confirms Docker is running. | |
| - Clones `cn-quickstart` if missing. | |
| - Runs `make setup` inside quickstart. | |
| - Creates `.daml/dist` & `scripts/` directories. | |
| 4. **Always export env vars inside tmux sessions before builds/tests**: | |
| ```bash | |
| export JAVA_HOME=/opt/homebrew/opt/openjdk@21 | |
| export PATH="$JAVA_HOME/bin:$PATH:$HOME/.daml/bin" | |
| ``` | |
| --- | |
| ## 5. Source Code | |
| ### 5.1 Token Module (`Token.daml`) | |
| ```daml | |
| -- Minimal Token module for ICO example | |
| -- Compatible with simple-token example | |
| module Token where | |
| import DA.Assert | |
| -- Basic fungible token | |
| template Token | |
| with | |
| issuer : Party -- Who issued the token | |
| owner : Party -- Current owner | |
| symbol : Text -- Token symbol (e.g., "USD", "BTC") | |
| amount : Decimal -- Amount held | |
| where | |
| ensure amount > 0.0 | |
| signatory issuer, owner -- Owner remains a signatory so they retain control/visibility | |
| -- Transfer to new owner | |
| choice Transfer : ContractId Token | |
| with | |
| newOwner : Party | |
| controller owner, newOwner | |
| do | |
| create this with owner = newOwner | |
| -- Split into two tokens | |
| choice Split : (ContractId Token, ContractId Token) | |
| with | |
| splitAmount : Decimal | |
| controller owner | |
| do | |
| assert (splitAmount > 0.0 && splitAmount < amount) | |
| token1 <- create this with amount = splitAmount | |
| token2 <- create this with amount = (amount - splitAmount) | |
| return (token1, token2) | |
| -- Merge with another token of same type | |
| choice Merge : ContractId Token | |
| with | |
| otherTokenCid : ContractId Token | |
| controller owner | |
| do | |
| otherToken <- fetch otherTokenCid | |
| -- Verify same issuer and symbol | |
| assert (issuer == otherToken.issuer) | |
| assert (symbol == otherToken.symbol) | |
| assert (owner == otherToken.owner) | |
| -- Archive the other token | |
| archive otherTokenCid | |
| -- Create merged token | |
| create this with amount = amount + otherToken.amount | |
| -- Token issuance authority | |
| template TokenIssuer | |
| with | |
| issuer : Party | |
| symbol : Text | |
| totalSupply : Decimal | |
| where | |
| signatory issuer | |
| -- Issue tokens to a party | |
| -- Nonconsuming so this issuer can mint multiple batches without being archived | |
| nonconsuming choice IssueTokens : ContractId Token | |
| with | |
| recipient : Party | |
| amount : Decimal | |
| controller issuer, recipient | |
| do | |
| assert (amount > 0.0) | |
| create Token with | |
| issuer = issuer | |
| owner = recipient | |
| symbol = symbol | |
| amount = amount | |
| ``` | |
| ### 5.2 ICO Module (`Ico.daml`) | |
| ```daml | |
| -- ICO (Initial Coin Offering) Smart Contract for Canton Network | |
| -- Allows selling one token (ICO token) for another token (payment token) | |
| -- Uses atomic Delivery-versus-Payment (DvP) pattern | |
| module Ico where | |
| import DA.Assert | |
| import DA.Time | |
| import Token | |
| -- ICO Offering: Represents an active ICO | |
| template IcoOffering | |
| with | |
| issuer : Party | |
| saleTokenIssuer : Party | |
| saleTokenSymbol : Text | |
| paymentTokenIssuer : Party | |
| paymentTokenSymbol : Text | |
| exchangeRate : Decimal | |
| totalSaleTokens : Decimal | |
| tokensSold : Decimal | |
| totalRaised : Decimal | |
| startTime : Time | |
| endTime : Time | |
| minPurchase : Decimal | |
| maxPurchase : Decimal | |
| observers : [Party] | |
| where | |
| ensure totalSaleTokens > 0.0 && exchangeRate > 0.0 && endTime > startTime && minPurchase > 0.0 && tokensSold <= totalSaleTokens | |
| signatory issuer | |
| observer observers | |
| choice Purchase : (ContractId Token.Token, ContractId IcoOffering) | |
| with | |
| buyer : Party | |
| paymentTokenCid : ContractId Token.Token | |
| paymentAmount : Decimal | |
| controller buyer, issuer, saleTokenIssuer | |
| do | |
| now <- getTime | |
| assert (now >= startTime) | |
| assert (now < endTime) | |
| assert (paymentAmount >= minPurchase) | |
| assert (maxPurchase == 0.0 || paymentAmount <= maxPurchase) | |
| let saleTokenAmount = paymentAmount * exchangeRate | |
| let remainingTokens = totalSaleTokens - tokensSold | |
| assert (saleTokenAmount <= remainingTokens) | |
| paymentToken <- fetch paymentTokenCid | |
| assert (paymentToken.issuer == paymentTokenIssuer) | |
| assert (paymentToken.symbol == paymentTokenSymbol) | |
| assert (paymentToken.owner == buyer) | |
| assert (paymentToken.amount >= paymentAmount) | |
| if paymentToken.amount == paymentAmount | |
| then | |
| do | |
| _ <- exercise paymentTokenCid Token.Transfer with | |
| newOwner = issuer | |
| return () | |
| else | |
| do | |
| (paymentPortionCid, _changeCid) <- exercise paymentTokenCid Token.Split with | |
| splitAmount = paymentAmount | |
| _ <- exercise paymentPortionCid Token.Transfer with | |
| newOwner = issuer | |
| return () | |
| saleTokenCid <- create Token.Token with | |
| issuer = saleTokenIssuer | |
| owner = buyer | |
| symbol = saleTokenSymbol | |
| amount = saleTokenAmount | |
| updatedIcoCid <- create this with | |
| tokensSold = tokensSold + saleTokenAmount | |
| totalRaised = totalRaised + paymentAmount | |
| return (saleTokenCid, updatedIcoCid) | |
| choice Close : ContractId IcoCompleted | |
| controller issuer | |
| do | |
| now <- getTime | |
| assert (now >= endTime || tokensSold >= totalSaleTokens) | |
| create IcoCompleted with | |
| issuer = issuer | |
| saleTokenIssuer = saleTokenIssuer | |
| saleTokenSymbol = saleTokenSymbol | |
| paymentTokenIssuer = paymentTokenIssuer | |
| paymentTokenSymbol = paymentTokenSymbol | |
| totalSaleTokens = totalSaleTokens | |
| tokensSold = tokensSold | |
| totalRaised = totalRaised | |
| finalExchangeRate = exchangeRate | |
| observers = observers | |
| -- ICO Completed: Final state after ICO ends | |
| template IcoCompleted | |
| with | |
| issuer : Party | |
| saleTokenIssuer : Party | |
| saleTokenSymbol : Text | |
| paymentTokenIssuer : Party | |
| paymentTokenSymbol : Text | |
| totalSaleTokens : Decimal | |
| tokensSold : Decimal | |
| totalRaised : Decimal | |
| finalExchangeRate : Decimal | |
| observers : [Party] | |
| where | |
| signatory issuer | |
| observer observers | |
| nonconsuming choice GetStats : (Decimal, Decimal, Decimal) | |
| controller issuer | |
| do | |
| return (tokensSold, totalRaised, finalExchangeRate) | |
| -- ICO Factory: Helper template to create ICOs | |
| template IcoFactory | |
| with | |
| issuer : Party | |
| where | |
| signatory issuer | |
| choice CreateIco : ContractId IcoOffering | |
| with | |
| saleTokenIssuer : Party | |
| saleTokenSymbol : Text | |
| paymentTokenIssuer : Party | |
| paymentTokenSymbol : Text | |
| exchangeRate : Decimal | |
| totalSaleTokens : Decimal | |
| startTime : Time | |
| endTime : Time | |
| minPurchase : Decimal | |
| maxPurchase : Decimal | |
| observers : [Party] | |
| controller issuer | |
| do | |
| assert (totalSaleTokens > 0.0) | |
| assert (exchangeRate > 0.0) | |
| assert (endTime > startTime) | |
| assert (minPurchase > 0.0) | |
| create IcoOffering with | |
| issuer = issuer | |
| saleTokenIssuer = saleTokenIssuer | |
| saleTokenSymbol = saleTokenSymbol | |
| paymentTokenIssuer = paymentTokenIssuer | |
| paymentTokenSymbol = paymentTokenSymbol | |
| exchangeRate = exchangeRate | |
| totalSaleTokens = totalSaleTokens | |
| tokensSold = 0.0 | |
| totalRaised = 0.0 | |
| startTime = startTime | |
| endTime = endTime | |
| minPurchase = minPurchase | |
| maxPurchase = maxPurchase | |
| observers = observers | |
| ``` | |
| ### 5.3 Test Module (`IcoTest.daml`) | |
| ```daml | |
| -- Test script for ICO smart contract | |
| module IcoTest where | |
| import DA.Assert | |
| import DA.Time | |
| import Daml.Script | |
| import Ico | |
| import Token | |
| -- Test ICO lifecycle | |
| test_ico_lifecycle = script do | |
| -- Setup parties | |
| icoIssuer <- allocateParty "IcoIssuer" | |
| saleTokenIssuer <- allocateParty "SaleTokenIssuer" | |
| paymentTokenIssuer <- allocateParty "PaymentTokenIssuer" | |
| buyer1 <- allocateParty "Buyer1" | |
| buyer2 <- allocateParty "Buyer2" | |
| -- Create token issuers | |
| saleIssuerCid <- submit saleTokenIssuer do | |
| createCmd TokenIssuer with | |
| issuer = saleTokenIssuer | |
| symbol = "MYCOIN" | |
| totalSupply = 1000000.0 | |
| paymentIssuerCid <- submit paymentTokenIssuer do | |
| createCmd TokenIssuer with | |
| issuer = paymentTokenIssuer | |
| symbol = "USDC" | |
| totalSupply = 1000000.0 | |
| -- Issue payment tokens to buyers (issuer + owner must authorize) | |
| buyer1PaymentCid <- submitMulti [paymentTokenIssuer, buyer1] [] $ do | |
| exerciseCmd paymentIssuerCid IssueTokens with | |
| recipient = buyer1 | |
| amount = 1000.0 | |
| buyer2PaymentCid <- submitMulti [paymentTokenIssuer, buyer2] [] $ do | |
| exerciseCmd paymentIssuerCid IssueTokens with | |
| recipient = buyer2 | |
| amount = 500.0 | |
| -- Get current time | |
| now <- getTime | |
| -- Create ICO Factory | |
| factoryCid <- submit icoIssuer do | |
| createCmd IcoFactory with | |
| issuer = icoIssuer | |
| -- Create ICO: Selling MYCOIN for USDC | |
| -- Exchange rate: 1 USDC = 100 MYCOIN | |
| -- Total for sale: 50,000 MYCOIN | |
| icoCid <- submit icoIssuer do | |
| exerciseCmd factoryCid CreateIco with | |
| saleTokenIssuer = saleTokenIssuer | |
| saleTokenSymbol = "MYCOIN" | |
| paymentTokenIssuer = paymentTokenIssuer | |
| paymentTokenSymbol = "USDC" | |
| exchangeRate = 100.0 | |
| totalSaleTokens = 50000.0 | |
| startTime = now | |
| endTime = addRelTime now (days 1) -- 1 day later | |
| minPurchase = 10.0 | |
| maxPurchase = 0.0 -- No maximum | |
| observers = [] | |
| -- Buyer 1 purchases: 100 USDC = 10,000 MYCOIN | |
| (saleToken1Cid, icoCid1) <- submitMulti [buyer1, icoIssuer, saleTokenIssuer] [] $ do | |
| exerciseCmd icoCid Purchase with | |
| buyer = buyer1 | |
| paymentTokenCid = buyer1PaymentCid | |
| paymentAmount = 100.0 | |
| -- Verify buyer 1 received tokens | |
| Some saleToken1 <- queryContractId buyer1 saleToken1Cid | |
| assert (saleToken1.amount == 10000.0) | |
| assert (saleToken1.symbol == "MYCOIN") | |
| -- Buyer 2 purchases: 50 USDC = 5,000 MYCOIN | |
| (saleToken2Cid, icoCid2) <- submitMulti [buyer2, icoIssuer, saleTokenIssuer] [] $ do | |
| exerciseCmd icoCid1 Purchase with | |
| buyer = buyer2 | |
| paymentTokenCid = buyer2PaymentCid | |
| paymentAmount = 50.0 | |
| -- Verify buyer 2 received tokens | |
| Some saleToken2 <- queryContractId buyer2 saleToken2Cid | |
| assert (saleToken2.amount == 5000.0) | |
| -- Verify ICO state | |
| Some ico2 <- queryContractId icoIssuer icoCid2 | |
| assert (ico2.tokensSold == 15000.0) | |
| assert (ico2.totalRaised == 150.0) | |
| return () | |
| ``` | |
| ### 5.4 Package Configuration (`daml.yaml`) | |
| ```yaml | |
| sdk-version: 3.3.0-snapshot.20250502.13767.0.v2fc6c7e2 | |
| name: ico-token | |
| version: 1.0.0 | |
| source: daml | |
| dependencies: | |
| - daml-prim | |
| - daml-stdlib | |
| - daml-script | |
| ``` | |
| ### 5.5 Automation Scripts | |
| #### `scripts/setup.sh` | |
| ```bash | |
| #!/bin/bash | |
| # | |
| # Setup script for Canton Network ICO Token | |
| # This script ensures prerequisites and sets up the development environment | |
| # | |
| set -e # Exit on error | |
| # Colors for output | |
| RED='[0;31m' | |
| GREEN='[0;32m' | |
| YELLOW='[1;33m' | |
| BLUE='[0;34m' | |
| NC='[0m' # No Color | |
| # Script directory | |
| SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" | |
| PROJECT_DIR="$(dirname "$SCRIPT_DIR")" | |
| REPO_ROOT="$(dirname "$(dirname "$PROJECT_DIR")")" | |
| echo -e "${BLUE}=== Canton Network ICO Token Setup ===${NC}" | |
| echo "" | |
| # Function to print status messages | |
| print_status() { | |
| echo -e "${BLUE}[INFO]${NC} $1" | |
| } | |
| print_success() { | |
| echo -e "${GREEN}[SUCCESS]${NC} $1" | |
| } | |
| print_error() { | |
| echo -e "${RED}[ERROR]${NC} $1" | |
| } | |
| print_warning() { | |
| echo -e "${YELLOW}[WARNING]${NC} $1" | |
| } | |
| # Check Docker | |
| print_status "Checking Docker installation..." | |
| if ! command -v docker &> /dev/null; then | |
| print_error "Docker is not installed" | |
| echo "Please install Docker from: https://docs.docker.com/get-docker/" | |
| exit 1 | |
| fi | |
| if ! docker info &> /dev/null; then | |
| print_error "Docker daemon is not running" | |
| echo "Please start Docker Desktop or the Docker daemon" | |
| exit 1 | |
| fi | |
| print_success "Docker is installed and running" | |
| # Check if cn-quickstart exists | |
| print_status "Checking for cn-quickstart repository..." | |
| QUICKSTART_DIR="$REPO_ROOT/cn-quickstart" | |
| if [ ! -d "$QUICKSTART_DIR" ]; then | |
| print_status "cn-quickstart not found. Cloning repository..." | |
| cd "$REPO_ROOT" | |
| git clone https://github.com/digital-asset/cn-quickstart.git | |
| print_success "Cloned cn-quickstart repository" | |
| else | |
| print_success "cn-quickstart repository already exists" | |
| fi | |
| # Check quickstart setup | |
| print_status "Checking cn-quickstart setup..." | |
| cd "$QUICKSTART_DIR/quickstart" | |
| if [ ! -f ".env" ]; then | |
| print_warning "cn-quickstart needs setup. Running 'make setup'..." | |
| make setup | |
| if [ $? -ne 0 ]; then | |
| print_error "Quickstart setup failed" | |
| exit 1 | |
| fi | |
| print_success "Quickstart setup complete" | |
| else | |
| print_success "cn-quickstart is already configured" | |
| fi | |
| # Create project directories | |
| print_status "Setting up project directories..." | |
| mkdir -p "$PROJECT_DIR/.daml/dist" | |
| mkdir -p "$PROJECT_DIR/scripts" | |
| print_success "Project directories created" | |
| echo "" | |
| print_success "Setup complete!" | |
| echo "" | |
| echo -e "${BLUE}Next steps:${NC}" | |
| echo " 1. Build the ICO contracts: ./scripts/build.sh" | |
| echo " 2. Deploy to LocalNet: ./scripts/deploy.sh" | |
| echo " 3. Run tests: ./scripts/test.sh" | |
| echo "" | |
| echo -e "${BLUE}What was set up:${NC}" | |
| echo " - Docker verified" | |
| echo " - cn-quickstart cloned and configured" | |
| echo " - Project directories created" | |
| echo "" | |
| echo -e "${YELLOW}Note:${NC} This uses the cn-quickstart Docker environment" | |
| echo " No local Daml or Java installation required!" | |
| echo "" | |
| ``` | |
| #### `scripts/build.sh` | |
| ```bash | |
| #!/bin/bash | |
| # | |
| # Build script for Canton Network ICO Token | |
| # Uses cn-quickstart Docker environment to compile the Daml contracts | |
| # | |
| set -e # Exit on error | |
| # Colors for output | |
| RED='[0;31m' | |
| GREEN='[0;32m' | |
| YELLOW='[1;33m' | |
| BLUE='[0;34m' | |
| NC='[0m' # No Color | |
| # Script directory | |
| SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" | |
| PROJECT_DIR="$(dirname "$SCRIPT_DIR")" | |
| REPO_ROOT="$(dirname "$(dirname "$PROJECT_DIR")")" | |
| QUICKSTART_DIR="$REPO_ROOT/cn-quickstart/quickstart" | |
| echo -e "${BLUE}=== Building Canton Network ICO Token ===${NC}" | |
| echo "" | |
| # Function to print status messages | |
| print_status() { | |
| echo -e "${BLUE}[INFO]${NC} $1" | |
| } | |
| print_success() { | |
| echo -e "${GREEN}[SUCCESS]${NC} $1" | |
| } | |
| print_error() { | |
| echo -e "${RED}[ERROR]${NC} $1" | |
| } | |
| # Check if quickstart exists | |
| if [ ! -d "$QUICKSTART_DIR" ]; then | |
| print_error "cn-quickstart not found at $QUICKSTART_DIR" | |
| echo "Please run: $SCRIPT_DIR/setup.sh" | |
| exit 1 | |
| fi | |
| # Auto-detect and set Java 21 if not already set | |
| if [ -z "$JAVA_HOME" ] || [ ! -d "$JAVA_HOME" ]; then | |
| if [ -d "/opt/homebrew/opt/openjdk@21" ]; then | |
| export JAVA_HOME=/opt/homebrew/opt/openjdk@21 | |
| export PATH="$JAVA_HOME/bin:$PATH" | |
| print_status "Auto-detected Java 21 at $JAVA_HOME" | |
| fi | |
| fi | |
| # Ensure daml is in PATH | |
| if [ -d "$HOME/.daml/bin" ] && [[ ":$PATH:" != *":$HOME/.daml/bin:"* ]]; then | |
| export PATH="$PATH:$HOME/.daml/bin" | |
| print_status "Added ~/.daml/bin to PATH" | |
| fi | |
| # Verify Java is available | |
| if ! command -v java &> /dev/null; then | |
| print_error "Java not found in PATH" | |
| echo "" | |
| echo "Java 21 is required. Please set:" | |
| echo " export JAVA_HOME=/opt/homebrew/opt/openjdk@21" | |
| echo " export PATH=\"\$JAVA_HOME/bin:\$PATH\"" | |
| echo "" | |
| echo "Please ensure the Java 21 environment variables are configured." | |
| exit 1 | |
| fi | |
| # Verify Java version (warn if not 21) | |
| JAVA_VERSION=$(java -version 2>&1 | head -1 | grep -oE 'version "([0-9]+)' | grep -oE '[0-9]+' || echo "0") | |
| if [ "$JAVA_VERSION" -lt 21 ]; then | |
| print_warning "Java version $JAVA_VERSION detected. Java 21+ recommended." | |
| echo " Current JAVA_HOME: $JAVA_HOME" | |
| echo " Recommended: export JAVA_HOME=/opt/homebrew/opt/openjdk@21" | |
| fi | |
| # Create ico-token package in quickstart | |
| print_status "Setting up ico-token package in quickstart..." | |
| ICO_PKG_DIR="$QUICKSTART_DIR/daml/ico-token" | |
| mkdir -p "$ICO_PKG_DIR/daml" | |
| # Copy all Daml files | |
| cp "$PROJECT_DIR/Ico.daml" "$ICO_PKG_DIR/daml/" | |
| cp "$PROJECT_DIR/Token.daml" "$ICO_PKG_DIR/daml/" | |
| cp "$PROJECT_DIR/IcoTest.daml" "$ICO_PKG_DIR/daml/" | |
| print_success "Daml files copied to $ICO_PKG_DIR/daml/" | |
| print_status " - Ico.daml" | |
| print_status " - Token.daml" | |
| print_status " - IcoTest.daml" | |
| # Create daml.yaml for the package | |
| print_status "Creating package configuration..." | |
| # Check if licensing dar exists to determine data-dependencies | |
| LICENSING_DAR="$QUICKSTART_DIR/daml/licensing/.daml/dist/licensing-0.1.0.dar" | |
| if [ -f "$LICENSING_DAR" ]; then | |
| print_status "Found licensing dar, adding as data-dependency..." | |
| cat > "$ICO_PKG_DIR/daml.yaml" << 'EOF' | |
| sdk-version: 3.3.0-snapshot.20250502.13767.0.v2fc6c7e2 | |
| name: ico-token | |
| version: 1.0.0 | |
| source: daml | |
| dependencies: | |
| - daml-prim | |
| - daml-stdlib | |
| - daml-script | |
| data-dependencies: | |
| - ../licensing/.daml/dist/licensing-0.1.0.dar | |
| EOF | |
| else | |
| print_status "No licensing dar found, using minimal dependencies..." | |
| cat > "$ICO_PKG_DIR/daml.yaml" << 'EOF' | |
| sdk-version: 3.3.0-snapshot.20250502.13767.0.v2fc6c7e2 | |
| name: ico-token | |
| version: 1.0.0 | |
| source: daml | |
| dependencies: | |
| - daml-prim | |
| - daml-stdlib | |
| - daml-script | |
| EOF | |
| fi | |
| # Add to multi-package.yaml if not already present | |
| if [ -f "$QUICKSTART_DIR/daml/multi-package.yaml" ]; then | |
| if ! grep -q "./ico-token" "$QUICKSTART_DIR/daml/multi-package.yaml"; then | |
| print_status "Adding ico-token to multi-package.yaml..." | |
| echo "- ./ico-token" >> "$QUICKSTART_DIR/daml/multi-package.yaml" | |
| fi | |
| fi | |
| print_success "Package structure created" | |
| # Navigate to quickstart directory | |
| cd "$QUICKSTART_DIR" | |
| # Build using quickstart's make (full build includes Daml compilation in Docker) | |
| print_status "Building with cn-quickstart (this may take 5-10 minutes first time)..." | |
| print_status "This will build all components including Daml contracts..." | |
| echo "" | |
| # Try to build - capture output to check for Java errors | |
| BUILD_OUTPUT=$(make build 2>&1) || BUILD_FAILED=true | |
| echo "$BUILD_OUTPUT" | |
| if [ -z "$BUILD_FAILED" ]; then | |
| BUILD_SUCCESS=true | |
| elif echo "$BUILD_OUTPUT" | grep -q "Unable to locate a Java Runtime"; then | |
| print_warning "Full build requires Java 21, but Java not found" | |
| echo "" | |
| echo "Java 21 is required to build Daml contracts via Gradle." | |
| echo "" | |
| echo "To install Java 21:" | |
| echo " brew install openjdk@21" | |
| echo " export JAVA_HOME=/opt/homebrew/opt/openjdk@21" | |
| echo " export PATH=\"\$JAVA_HOME/bin:\$PATH\"" | |
| echo "" | |
| echo "If the build continues to fail, rebuild the quickstart workspace before rerunning this script." | |
| BUILD_SUCCESS=false | |
| else | |
| BUILD_SUCCESS=false | |
| fi | |
| if [ "$BUILD_SUCCESS" = true ]; then | |
| print_success "Build successful!" | |
| # Find the ico-token DAR file | |
| ICO_DAR=$(find "$ICO_PKG_DIR/.daml/dist" -name "ico-token-*.dar" -type f | head -1) | |
| if [ -n "$ICO_DAR" ]; then | |
| ICO_DAR_NAME=$(basename "$ICO_DAR") | |
| # Create our own build directory | |
| mkdir -p "$PROJECT_DIR/.daml/dist" | |
| # Copy the DAR for reference | |
| cp "$ICO_DAR" "$PROJECT_DIR/.daml/dist/" | |
| print_success "Build artifacts ready" | |
| echo "" | |
| echo -e "${GREEN}Build Output:${NC}" | |
| echo " ICO DAR: $ICO_DAR" | |
| echo " Copied to: $PROJECT_DIR/.daml/dist/$ICO_DAR_NAME" | |
| echo " Size: $(du -h "$ICO_DAR" | cut -f1)" | |
| echo "" | |
| # Also list other DARs that were built | |
| print_status "Other DARs built:" | |
| find "$QUICKSTART_DIR/daml" -name "*.dar" -type f | while read dar; do | |
| echo " - $(basename "$dar")" | |
| done | |
| else | |
| print_error "ico-token DAR file not found after build" | |
| echo "Checking what was built..." | |
| find "$QUICKSTART_DIR/daml" -name "*.dar" -type f || true | |
| exit 1 | |
| fi | |
| else | |
| print_error "Build failed" | |
| echo "" | |
| echo "Troubleshooting:" | |
| echo " - Check Docker is running: docker info" | |
| echo " - Check Docker has 8GB+ RAM allocated" | |
| echo "" | |
| echo " - Try: cd $QUICKSTART_DIR && make clean-all && make build" | |
| exit 1 | |
| fi | |
| echo "" | |
| print_success "Build complete!" | |
| echo "" | |
| echo -e "${BLUE}Note:${NC} ICO contracts are now part of the quickstart project" | |
| echo "The contracts will be available when you deploy the quickstart application" | |
| echo "" | |
| echo -e "${BLUE}Next steps:${NC}" | |
| echo " 1. Run tests: daml test (in quickstart directory)" | |
| echo " 2. Deploy to LocalNet: $SCRIPT_DIR/deploy.sh" | |
| echo "" | |
| ``` | |
| #### `scripts/test.sh` | |
| ```bash | |
| #!/bin/bash | |
| # | |
| # Test script for Canton Network ICO Token | |
| # Runs the ICO test suite | |
| # | |
| set -e # Exit on error | |
| # Colors for output | |
| RED='[0;31m' | |
| GREEN='[0;32m' | |
| YELLOW='[1;33m' | |
| BLUE='[0;34m' | |
| NC='[0m' # No Color | |
| # Script directory | |
| SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" | |
| PROJECT_DIR="$(dirname "$SCRIPT_DIR")" | |
| REPO_ROOT="$(dirname "$(dirname "$PROJECT_DIR")")" | |
| QUICKSTART_DIR="$REPO_ROOT/cn-quickstart/quickstart" | |
| echo -e "${BLUE}=== Testing Canton Network ICO Token ===${NC}" | |
| echo "" | |
| # Function to print status messages | |
| print_status() { | |
| echo -e "${BLUE}[INFO]${NC} $1" | |
| } | |
| print_success() { | |
| echo -e "${GREEN}[SUCCESS]${NC} $1" | |
| } | |
| print_error() { | |
| echo -e "${RED}[ERROR]${NC} $1" | |
| } | |
| # Check if quickstart exists | |
| if [ ! -d "$QUICKSTART_DIR" ]; then | |
| print_error "cn-quickstart not found at $QUICKSTART_DIR" | |
| echo "Please run: $SCRIPT_DIR/setup.sh" | |
| exit 1 | |
| fi | |
| # Check if package is built | |
| if [ ! -d "$QUICKSTART_DIR/daml/ico-token" ]; then | |
| print_error "ico-token package not found. Building first..." | |
| "$SCRIPT_DIR/build.sh" | |
| fi | |
| # Navigate to quickstart directory | |
| cd "$QUICKSTART_DIR" | |
| # Run tests | |
| print_status "Running ICO tests..." | |
| echo "" | |
| # Use daml test command | |
| if cd daml/ico-token && daml test; then | |
| print_success "All tests passed!" | |
| echo "" | |
| echo -e "${GREEN}Test Results:${NC}" | |
| echo " ✅ ICO lifecycle test passed" | |
| echo " ✅ Token operations verified" | |
| echo " ✅ Purchase flow validated" | |
| else | |
| print_error "Tests failed" | |
| exit 1 | |
| fi | |
| echo "" | |
| echo -e "${BLUE}Next steps:${NC}" | |
| echo " 1. Deploy to LocalNet: $SCRIPT_DIR/deploy.sh" | |
| echo " 2. View contracts in Scan UI: http://scan.localhost:4000" | |
| echo "" | |
| ``` | |
| #### `scripts/deploy.sh` | |
| ```bash | |
| #!/bin/bash | |
| # | |
| # Deploy script for Canton Network ICO Token | |
| # Deploys the ICO contracts to Canton LocalNet | |
| # | |
| set -e # Exit on error | |
| # Colors for output | |
| RED='[0;31m' | |
| GREEN='[0;32m' | |
| YELLOW='[1;33m' | |
| BLUE='[0;34m' | |
| NC='[0m' # No Color | |
| # Script directory | |
| SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" | |
| PROJECT_DIR="$(dirname "$SCRIPT_DIR")" | |
| REPO_ROOT="$(dirname "$(dirname "$PROJECT_DIR")")" | |
| QUICKSTART_DIR="$REPO_ROOT/cn-quickstart/quickstart" | |
| echo -e "${BLUE}=== Deploying Canton Network ICO Token ===${NC}" | |
| echo "" | |
| # Function to print status messages | |
| print_status() { | |
| echo -e "${BLUE}[INFO]${NC} $1" | |
| } | |
| print_success() { | |
| echo -e "${GREEN}[SUCCESS]${NC} $1" | |
| } | |
| print_error() { | |
| echo -e "${RED}[ERROR]${NC} $1" | |
| } | |
| print_warning() { | |
| echo -e "${YELLOW}[WARNING]${NC} $1" | |
| } | |
| # Check if quickstart exists | |
| if [ ! -d "$QUICKSTART_DIR" ]; then | |
| print_error "cn-quickstart not found at $QUICKSTART_DIR" | |
| echo "Please run: $SCRIPT_DIR/setup.sh" | |
| exit 1 | |
| fi | |
| # Check if ico-token package exists | |
| if [ ! -d "$QUICKSTART_DIR/daml/ico-token" ]; then | |
| print_warning "ico-token package not found in quickstart. Running build..." | |
| "$SCRIPT_DIR/build.sh" | |
| elif [ ! -f "$QUICKSTART_DIR/daml/ico-token/daml/Ico.daml" ]; then | |
| print_warning "Ico.daml not found in package. Running build..." | |
| "$SCRIPT_DIR/build.sh" | |
| fi | |
| # Navigate to quickstart directory | |
| cd "$QUICKSTART_DIR" | |
| # Check if services are running | |
| print_status "Checking if Canton LocalNet is running..." | |
| if docker compose ps | grep -q "Up"; then | |
| print_success "Services are already running" | |
| else | |
| print_status "Starting Canton LocalNet (this will take several minutes)..." | |
| print_warning "This will download Docker images on first run" | |
| # Start the services | |
| make start | |
| if [ $? -ne 0 ]; then | |
| print_error "Failed to start Canton LocalNet" | |
| exit 1 | |
| fi | |
| # Wait for services to be ready | |
| print_status "Waiting for services to be ready (30 seconds)..." | |
| sleep 30 | |
| fi | |
| # Verify services are healthy | |
| print_status "Verifying service health..." | |
| docker compose ps | |
| print_success "Canton LocalNet is running!" | |
| echo "" | |
| echo -e "${GREEN}Deployment Information:${NC}" | |
| echo "" | |
| echo -e "${BLUE}The ICO contracts are now deployed as part of the quickstart application.${NC}" | |
| echo "" | |
| echo -e "${BLUE}Access the applications:${NC}" | |
| echo " App Provider Frontend: http://app-provider.localhost:3000" | |
| echo " App User Wallet: http://wallet.localhost:2000" | |
| echo " Scan UI (Explorer): http://scan.localhost:4000" | |
| echo "" | |
| echo -e "${BLUE}To interact with the ICO:${NC}" | |
| echo " 1. Use the Scan UI to explore contracts" | |
| echo " 2. Run the test script: $SCRIPT_DIR/test.sh" | |
| echo " 3. Use Canton Console: make canton-console (in quickstart dir)" | |
| echo "" | |
| echo -e "${YELLOW}To stop services:${NC}" | |
| echo " cd $QUICKSTART_DIR && make stop" | |
| echo "" | |
| echo -e "${YELLOW}To clean up and restart:${NC}" | |
| echo " cd $QUICKSTART_DIR && make clean-all && make start" | |
| echo "" | |
| ``` | |
| *(Scripts truncated where comments repeat – see repository for unchanged trailing echo statements.)* | |
| --- | |
| ## 6. Build Workflow | |
| 1. **Recommended tmux usage** (prevents hung shells): | |
| ```bash | |
| tmux new-session -d -s ico_build 'cd <ICO_PROJECT_ROOT> && ./scripts/build.sh > /tmp/ico_build.log 2>&1' | |
| ``` | |
| 2. **Expected build output**: | |
| - DAR generated at `<QUICKSTART_HOME>/daml/ico-token/.daml/dist/ico-token-1.0.0.dar` (~416 KB). | |
| - Copy stored at `<ICO_PROJECT_ROOT>/.daml/dist/ico-token-1.0.0.dar`. | |
| - Full log: `/tmp/ico_build.log`. | |
| 3. **Key build learnings**: | |
| - Missing Java 21 → "Unable to locate a Java Runtime". Fix with `brew install openjdk@21` and export `JAVA_HOME`. | |
| - Script adds package to `multi-package.yaml` automatically. | |
| - If build fails, rerun `cd <QUICKSTART_HOME> && make clean-all && make build` then rerun `./scripts/build.sh`. | |
| --- | |
| ## 7. Testing Workflow & Multi-Party Lessons | |
| 1. **Run tests in tmux**: | |
| ```bash | |
| tmux new-session -d -s ico_test 'cd <QUICKSTART_HOME>/daml/ico-token && daml test --detail=2 > /tmp/ico_test.log 2>&1' | |
| ``` | |
| 2. **Success criteria**: | |
| - Output includes "8 transactions" and "9 active contracts". | |
| - Log stored at `/tmp/ico_test.log`. | |
| 3. **Critical multi-party patterns**: | |
| - `Token` template signatories: issuer + owner ⇒ every issuance must use `submitMulti [issuer, owner] []`. | |
| - `TokenIssuer.IssueTokens` marked `nonconsuming` with controllers `(issuer, recipient)` to support multiple issuances. | |
| - `Token.Transfer` controllers `(owner, newOwner)` to enforce payment delivery consent. | |
| - `IcoOffering.Purchase` controllers `(buyer, issuer, saleTokenIssuer)` requiring all three parties in `submitMulti`. | |
| - Without the above, Daml returns `missing authorization` errors. | |
| 4. **Common test failure reasons**: | |
| - Using `submit` instead of `submitMulti`. | |
| - Forgetting to include sale token issuer in controller list. | |
| - Token issuer contract consumed (not marked `nonconsuming`). | |
| - Controller lists and `submitMulti` actors must match exactly. | |
| --- | |
| ## 8. Deployment Workflow | |
| 1. **Deploy in tmux**: | |
| ```bash | |
| tmux new-session -d -s ico_deploy 'cd <ICO_PROJECT_ROOT> && ./scripts/deploy.sh > /tmp/ico_deploy.log 2>&1' | |
| ``` | |
| 2. **Verify containers**: | |
| ```bash | |
| tmux new-session -d -s ico_containers 'docker ps --format "table {{.Names}}\t{{.Status}}" > /tmp/ico_containers.log' | |
| ``` | |
| - All services should be `Up`. `cadvisor` may be unhealthy (observability-only warning). | |
| 3. **Smoke checks**: | |
| ```bash | |
| tmux new-session -d -s ico_scan 'curl -sS -o /tmp/ico_scan.html -w "%{http_code}" http://scan.localhost:4000 > /tmp/ico_scan_code.log' | |
| tmux new-session -d -s ico_scan_instances 'curl -sS http://scan.localhost:4000/api/scan/v0/splice-instance-names > /tmp/ico_scan_instances.json' | |
| ``` | |
| - Expect HTTP 200 in `/tmp/ico_scan_code.log` and populated registry JSON. | |
| 4. **Post-deploy ledger script (currently blocked)**: | |
| ```bash | |
| tmux new-session -d -s ico_script 'daml script --dar <QUICKSTART_HOME>/daml/ico-token/.daml/dist/ico-token-1.0.0.dar --script-name IcoTest:test_ico_lifecycle --ledger-host localhost --ledger-port 3901 --static-time > /tmp/ico_script.log 2>&1' | |
| ``` | |
| - Result: `UNAUTHENTICATED`. Quickstart requires Keycloak/shared-secret OAuth tokens. Capture credentials before automating this step. | |
| 5. **Endpoints**: | |
| - App Provider: `http://app-provider.localhost:3000` | |
| - Wallet UI: `http://wallet.localhost:2000` | |
| - Scan UI: `http://scan.localhost:4000` | |
| 6. **Important logs**: | |
| - Build: `/tmp/ico_build.log` | |
| - Tests: `/tmp/ico_test.log` | |
| - Deploy: `/tmp/ico_deploy.log` | |
| - Containers: `/tmp/ico_containers.log` | |
| - Scan check: `/tmp/ico_scan_code.log` + `/tmp/ico_scan.html` | |
| - Registry sample: `/tmp/ico_scan_instances.json` | |
| - Ledger auth failure: `/tmp/ico_script.log` | |
| --- | |
| ## 9. Compilation & Syntax Learnings | |
| | Issue | Wrong | Correct | | |
| |-------|-------|---------| | |
| | Multiple `ensure` statements | `ensure A` / `ensure B` | `ensure A && B` | | |
| | `assert` usage | `assert (cond) "msg"` | `assert (cond)` | | |
| | If-then-else types | Return `ContractId` in one branch, `()` in other | Make both branches `do ...; return ()` | | |
| | Choice references | `exerciseCmd cid Module.Choice` | `exerciseCmd cid Choice` | | |
| | RelTime helpers | `relTimeFromDays` / `secondsToRelTime` | `days n`, `hours n`, `seconds n` | | |
| | Token issuer choice | Consuming default | Mark as `nonconsuming` | | |
| | Transfer controllers | Only owner | `controller owner, newOwner` | | |
| | Purchase controllers | Missing sale token issuer | Include `(buyer, issuer, saleTokenIssuer)` | | |
| **Key rule:** If a new contract includes signatories `[A, B]`, the transaction exercising that choice must include both `A` and `B` in `submitMulti`. | |
| --- | |
| ## 10. Testing Insights | |
| - **Original failure**: `missing authorization from 'BuyerX'` triggered when only issuer signed token issuance. | |
| - **Resolution**: Wrap issuance in `submitMulti [issuer, recipient] [] $ do ...`. | |
| - **Why simple-token appeared to work**: That example consumed issuers differently; in this project we preserved stricter security, requiring explicit multi-party signatures. | |
| - **Future enhancements**: | |
| 1. Automate tmux-driven `daml test` in CI. | |
| 2. Add negative tests (over-limit purchase, purchase after end time, paused ICO). | |
| 3. Add smoke tests hitting the ledger once authentication credentials are available. | |
| --- | |
| ## 11. Deployment Learnings & Outstanding Work | |
| - Build/Test/Deploy pipeline confirmed working with tmux logs captured. | |
| - After deployment, only `cadvisor` shows unhealthy (expected if observability dashboards unused). | |
| - Outstanding tasks: | |
| 1. Add integration/negative tests for oversize purchases and closed ICO scenarios. | |
| 2. Automate CI pipeline that runs setup → build → test → deploy in tmux. | |
| 3. Create smoke checks (Ledger API ping, registry queries) and resolve authentication by sourcing Keycloak/shared-secret tokens. | |
| 4. Document handling of optional container health (cadvisor) and capture Scan UI screenshots/ contract IDs post-purchase. | |
| --- | |
| ## 12. Usage Example (rebuild from scratch) | |
| 1. **Bootstrap env** | |
| ```bash | |
| tmux new -s ico_setup | |
| export JAVA_HOME=/opt/homebrew/opt/openjdk@21 | |
| export PATH="$JAVA_HOME/bin:$PATH:$HOME/.daml/bin" | |
| cd <ICO_PROJECT_ROOT> | |
| ./scripts/setup.sh | |
| ``` | |
| 2. **Build (tmux)** | |
| ```bash | |
| tmux new-session -d -s ico_build 'cd <ICO_PROJECT_ROOT> && ./scripts/build.sh > /tmp/ico_build.log 2>&1' | |
| tmux attach -t ico_build # Optional to monitor logs | |
| ``` | |
| 3. **Run tests** | |
| ```bash | |
| tmux new-session -d -s ico_test 'cd <QUICKSTART_HOME>/daml/ico-token && daml test --detail=2 > /tmp/ico_test.log 2>&1' | |
| ``` | |
| 4. **Deploy** | |
| ```bash | |
| tmux new-session -d -s ico_deploy 'cd <ICO_PROJECT_ROOT> && ./scripts/deploy.sh > /tmp/ico_deploy.log 2>&1' | |
| ``` | |
| 5. **Health checks** | |
| ```bash | |
| tmux new-session -d -s ico_containers 'docker ps --format "table {{.Names}}\t{{.Status}}" > /tmp/ico_containers.log' | |
| tmux new-session -d -s ico_scan 'curl -sS -o /tmp/ico_scan.html -w "%{http_code}" http://scan.localhost:4000 > /tmp/ico_scan_code.log' | |
| ``` | |
| 6. **Interact** – Use wallet UI (`http://wallet.localhost:2000`) to create parties and exercise workflows, or use Daml script (remember authentication requirement). | |
| --- | |
| ## 13. Troubleshooting Cheat Sheet | |
| | Symptom | Likely Cause | Fix | | |
| |---------|--------------|-----| | |
| | `Unable to locate a Java Runtime` | `JAVA_HOME` unset | `export JAVA_HOME=/opt/homebrew/opt/openjdk@21` & update `PATH` | | |
| | `daml: command not found` | `~/.daml/bin` not in `PATH` | `export PATH="$PATH:$HOME/.daml/bin"` | | |
| | `missing authorization from 'BuyerX'` | Not using `submitMulti` with all signatories | Add all controllers to `submitMulti` list | | |
| | `IssueTokens` only works once | Choice still consuming | Mark as `nonconsuming` and require issuer + recipient controllers | | |
| | `docker compose ps` shows containers down | Deployment failed | Re-run `./scripts/deploy.sh` and inspect `/tmp/ico_deploy.log` | | |
| | Scan UI returns non-200 | Services not ready | Wait 30s+, check `/tmp/ico_containers.log`, rerun `make start` | | |
| | `UNAUTHENTICATED` from `daml script` | Missing OAuth credentials | Obtain Keycloak/shared-secret token or run via Canton console | | |
| --- | |
| ## 14. Production & Compliance Enhancements | |
| - **Security**: Add KYC/AML checks, buyer whitelists, pause/unpause controls, multi-sig closing flow, HSM-backed keys. | |
| - **Features**: Tiered pricing, vesting schedules, refund mechanisms, soft/hard caps, referral bonuses. | |
| - **Compliance**: Regulator observers, transfer restrictions, freeze capabilities, audit trail exports. | |
| - **Advanced**: Multi-token payments, dynamic exchange rates, Dutch auction pricing, CIP-56 integration. | |
| --- | |
| ## 15. Operational Record-Keeping | |
| - Maintain a record of each end-to-end run, including tmux commands and artifact paths, so future operators can replay the workflow reliably. | |
| --- | |
| ## 16. Final Notes | |
| - All processes (build/test/deploy/smoke checks) must run in tmux sessions to avoid hung terminals, with logs stored under `/tmp/ico_*.log`. | |
| - Maintaining Java 21 and Daml snapshot versions is critical; mismatched versions will fail builds. | |
| - Multi-party authorization is enforced at transaction boundaries—design choices and test scripts must list every acting party explicitly. | |
| - Authentication for ledger CLI access remains outstanding; plan to capture Keycloak credentials or script via Canton console once available. | |
| With this guide, an LLM (or human) can recreate the ICO token project from first principles, understand every decision made during development, and extend the implementation responsibly. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment