diff --git a/sdk/templates/multi-party-agreement/Readme.md b/sdk/templates/multi-party-agreement/Readme.md new file mode 100644 index 000000000000..646b3294cd8f --- /dev/null +++ b/sdk/templates/multi-party-agreement/Readme.md @@ -0,0 +1,101 @@ +# Multi-Party Agreement Tutorial + +Learn Canton's unique party-based authorization model by building a collaborative agreement contract. + +## What You'll Learn + +- How Canton's **party model** differs from address-based blockchains +- Dynamic signatory lists and authorization +- Observer pattern for selective visibility +- Multi-party workflows without complex coordination + +## The Problem + +On public blockchains like Ethereum, authorization is simple: if you have the private key for an address, you can sign transactions. But what if you need multiple parties to coordinate on a decision? + +Canton solves this with a **party-based authorization model** where contracts explicitly declare who must authorize actions. + +## The Contract + +```daml +template MultiPartyAgreement + with + proposer : Party + signatories : [Party] + requiredParties : [Party] + terms : Text + where + signatory signatories + observer requiredParties +``` + +### Key Concepts + +**Signatories** (`signatory signatories`) +- Parties who have already signed the agreement +- ALL signatories must authorize any changes to this contract +- This is a **list that grows** as parties join + +**Observers** (`observer requiredParties`) +- Parties who can see the contract but haven't signed yet +- They need visibility to exercise the `AddParty` choice +- Without observer status, Bob couldn't even see the agreement to join it + +**Why both?** +- `signatories` = who has committed +- `requiredParties` = who is invited but hasn't committed yet + +## The Workflow + +### Step 1: Alice Proposes + +```daml +agreementCid <- submit alice do + createCmd MultiPartyAgreement with + proposer = alice + signatories = [alice] -- Only Alice has signed + requiredParties = [bob, carol] -- Bob and Carol can see it + terms = "We agree to collaborate on this project" +``` + +**What happens:** +- Alice creates the agreement +- She's the only signatory (she authorized creation) +- Bob and Carol are observers (they can see it but haven't signed) + +### Step 2: Bob Joins + +```daml +agreementCid <- submit bob do + exerciseCmd agreementCid AddParty with newParty = bob +``` + +**What happens:** +- Bob exercises the `AddParty` choice +- The choice controller is `newParty` (Bob), so he must authorize +- A new contract is created with `signatories = [bob, alice]` +- The old contract is archived (consumed by the choice) + +### Step 3: Carol Joins + +```daml +submit carol do + exerciseCmd agreementCid AddParty with newParty = carol +``` + +**Final state:** +- `signatories = [carol, bob, alice]` +- All three parties have now authorized the agreement + +## The Choice + +```daml +choice AddParty : ContractId MultiPartyAgreement + with + newParty : Party + controller newParty + do + assertMsg "Party not in required list" (newParty `elem` requiredParties) + assertMsg "Party already signed" (newParty `notElem` signatories) + create this with signatories = newParty :: signatories +``` \ No newline at end of file diff --git a/sdk/templates/multi-party-agreement/daml.yaml.template b/sdk/templates/multi-party-agreement/daml.yaml.template new file mode 100644 index 000000000000..6892c7cc8bf5 --- /dev/null +++ b/sdk/templates/multi-party-agreement/daml.yaml.template @@ -0,0 +1,8 @@ +sdk-version: __VERSION__ +name: __PROJECT_NAME__ +source: daml/Agreement.daml +version: 1.3.0 +dependencies: + - daml-prim + - daml-stdlib + - daml-script \ No newline at end of file diff --git a/sdk/templates/multi-party-agreement/daml/Agreement.daml b/sdk/templates/multi-party-agreement/daml/Agreement.daml new file mode 100644 index 000000000000..08338428faaf --- /dev/null +++ b/sdk/templates/multi-party-agreement/daml/Agreement.daml @@ -0,0 +1,52 @@ +module Agreement where + +import Daml.Script + +template MultiPartyAgreement + with + proposer : Party + signatories : [Party] + requiredParties : [Party] + terms : Text + where + signatory signatories + observer requiredParties + + choice AddParty : ContractId MultiPartyAgreement + with + newParty : Party + controller newParty + do + assertMsg "Party not in required list" (newParty `elem` requiredParties) + assertMsg "Party already signed" (newParty `notElem` signatories) + create this with signatories = newParty :: signatories + + choice Close : () + controller proposer + do + pure () + +-- Tests +test_agreement : Script () +test_agreement = script do + alice <- allocateParty "Alice" + bob <- allocateParty "Bob" + carol <- allocateParty "Carol" + + -- Alice proposes agreement + agreementCid <- submit alice do + createCmd MultiPartyAgreement with + proposer = alice + signatories = [alice] + requiredParties = [bob, carol] + terms = "We agree to collaborate on this project" + + -- Bob joins + agreementCid <- submit bob do + exerciseCmd agreementCid AddParty with newParty = bob + + -- Carol joins + submit carol do + exerciseCmd agreementCid AddParty with newParty = carol + + pure () diff --git a/sdk/templates/simple-token-utility/README.md b/sdk/templates/simple-token-utility/README.md new file mode 100644 index 000000000000..6a748823c030 --- /dev/null +++ b/sdk/templates/simple-token-utility/README.md @@ -0,0 +1,104 @@ +# Simple Token Tutorial + +Learn how to build fungible tokens on Canton using patterns from the CIP-56 Canton Token Standard. + +## What You'll Learn + +- UTXO-style asset management +- Proposal/acceptance pattern for transfers (CIP-56) +- Split and merge operations +- Token holder privacy model +- Observer pattern for issuer oversight + +## The Token Holding + +```daml +template TokenHolding + with + issuer : Party + owner : Party + amount : Decimal + instrument : Text + where + signatory owner + observer issuer +``` + +### Key Design Decisions + +**Why owner is the only signatory?** +- Owner controls their assets +- Can propose transfers without issuer approval +- Standard pattern for bearer tokens + +**Why issuer is an observer?** +- Issuer sees all holdings (for compliance, total supply) +- Issuer doesn't control transfers (can't freeze without owner consent) +- Balance between privacy and transparency +agement | + +## The Transfer Flow (CIP-56 Pattern) + +### Why Proposal + Acceptance? + +**Problem:** Alice can't just create a holding for Bob +```daml +-- This FAILS - Bob must authorize becoming owner (signatory) +create TokenHolding with owner = bob, ... +``` + +**Solution:** Two-step workflow + +### Step 1: Alice Proposes Transfer + +```daml +choice ProposeTransfer : ContractId TransferProposal + with + newOwner : Party + transferAmount : Decimal + controller owner + do + -- Create remainder for Alice + if transferAmount < amount + then create this with amount = amount - transferAmount + else pure () + + -- Create proposal for Bob + create TransferProposal with + sender = owner + receiver = newOwner + transferAmount + ... +``` + +**What happens:** +- Alice's 100 token holding is **consumed** (archived) +- A 70 token holding is **created** for Alice (remainder) +- A **transfer proposal** is created for Bob to accept + +### Step 2: Bob Accepts + +```daml +template TransferProposal + where + signatory sender + observer receiver -- Bob can see the proposal + + choice Accept : ContractId TokenHolding + controller receiver + do + create TokenHolding with + owner = receiver + amount = transferAmount + ... +``` + +**What happens:** +- Bob exercises `Accept` (he authorizes) +- A 30 token holding is **created** for Bob +- The proposal is **consumed** + +**Final state:** +- Alice: 70 token holding +- Bob: 30 token holding +- Both authorized their own holdings \ No newline at end of file diff --git a/sdk/templates/simple-token-utility/daml.yaml.template b/sdk/templates/simple-token-utility/daml.yaml.template new file mode 100644 index 000000000000..9f6735fa0946 --- /dev/null +++ b/sdk/templates/simple-token-utility/daml.yaml.template @@ -0,0 +1,8 @@ +sdk-version: __VERSION__ +name: __PROJECT_NAME__ +source: daml/Token.daml +version: 1.3.0 +dependencies: + - daml-prim + - daml-stdlib + - daml-script \ No newline at end of file diff --git a/sdk/templates/simple-token-utility/daml/Token.daml b/sdk/templates/simple-token-utility/daml/Token.daml new file mode 100644 index 000000000000..08c2ca5743ae --- /dev/null +++ b/sdk/templates/simple-token-utility/daml/Token.daml @@ -0,0 +1,134 @@ +-- Simple token implementation inspired by CIP-56 Canton Token Standard +-- This is a learning example - for production use the actual CIP-56 interfaces +module Token where + +import Daml.Script + +template TokenHolding + with + issuer : Party + owner : Party + amount : Decimal + instrument : Text -- e.g. "CC" + where + signatory owner + observer issuer + + ensure amount > 0.0 + + -- Propose a transfer to another party + choice ProposeTransfer : ContractId TransferProposal + with + newOwner : Party + transferAmount : Decimal + controller owner + do + assertMsg "Transfer amount must be positive" (transferAmount > 0.0) + assertMsg "Insufficient balance" (transferAmount <= amount) + + -- Create remainder for sender if needed + if transferAmount < amount + then do + create this with amount = amount - transferAmount + pure () + else pure () + + -- Create transfer proposal + create TransferProposal with + sender = owner + receiver = newOwner + transferAmount + remainderAmount = amount - transferAmount + instrument + issuer + + -- Split holding into two separate holdings + choice Split : (ContractId TokenHolding, ContractId TokenHolding) + with + splitAmount : Decimal + controller owner + do + assertMsg "Split amount must be positive" (splitAmount > 0.0) + assertMsg "Split amount must be less than total" (splitAmount < amount) + + holding1 <- create this with amount = splitAmount + holding2 <- create this with amount = amount - splitAmount + pure (holding1, holding2) + + -- Merge with another holding of the same instrument + choice Merge : ContractId TokenHolding + with + otherHoldingCid : ContractId TokenHolding + controller owner + do + otherHolding <- fetch otherHoldingCid + assertMsg "Owners must match" (otherHolding.owner == owner) + assertMsg "Issuers must match" (otherHolding.issuer == issuer) + assertMsg "Instruments must match" (otherHolding.instrument == instrument) + + archive otherHoldingCid + create this with amount = amount + otherHolding.amount + +-- Transfer proposal that receiver must accept +template TransferProposal + with + sender : Party + receiver : Party + transferAmount : Decimal + remainderAmount : Decimal + instrument : Text + issuer : Party + where + signatory sender + observer receiver + + choice Accept : ContractId TokenHolding + controller receiver + do + -- Create holding for receiver + create TokenHolding with + owner = receiver + amount = transferAmount + instrument + issuer + + choice Reject : () + controller receiver + do + pure () + +-- Test script +test_token : Script () +test_token = script do + -- Parties + issuer <- allocateParty "TokenIssuer" + alice <- allocateParty "Alice" + bob <- allocateParty "Bob" + + -- Alice receives initial token holding + aliceHolding <- submit alice do + createCmd TokenHolding with + issuer + owner = alice + amount = 100.0 + instrument = "USD" + + -- Alice proposes transfer of 30 tokens to Bob + proposalCid <- submit alice do + exerciseCmd aliceHolding ProposeTransfer with + newOwner = bob + transferAmount = 30.0 + + -- Bob accepts the transfer + bobHolding <- submit bob do + exerciseCmd proposalCid Accept + + -- Bob splits his holding + (bob1, bob2) <- submit bob do + exerciseCmd bobHolding Split with splitAmount = 10.0 + + -- Bob merges them back + submit bob do + exerciseCmd bob1 Merge with otherHoldingCid = bob2 + + pure ()