Skip to content

Instantly share code, notes, and snippets.

@crazyrabbitLTC
Created November 12, 2025 17:16
Show Gist options
  • Select an option

  • Save crazyrabbitLTC/75a7d45a15ae473d239cd771e2ac6e45 to your computer and use it in GitHub Desktop.

Select an option

Save crazyrabbitLTC/75a7d45a15ae473d239cd771e2ac6e45 to your computer and use it in GitHub Desktop.
Canton Network ICO Token Project LLMs.txt
# 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=''
GREEN=''
YELLOW=''
BLUE=''
NC='' # 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=''
GREEN=''
YELLOW=''
BLUE=''
NC='' # 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=''
GREEN=''
YELLOW=''
BLUE=''
NC='' # 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=''
GREEN=''
YELLOW=''
BLUE=''
NC='' # 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