create-canton-app 1.4.2 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.vscode/settings.json +5 -0
- package/README.md +2 -2
- package/package.json +1 -1
- package/src/commands/create.js +3 -3
- package/src/templates/AssetOwner/daml/AssetHolding.daml +276 -0
- package/src/templates/AssetOwner/daml.yaml +18 -0
- package/src/templates/Multiparty/daml/MultiPartyAgreement.daml +256 -0
- package/src/templates/Multiparty/daml.yaml +15 -0
- package/src/templates/TokenTransfer/daml/Token.daml +222 -0
- package/src/templates/TokenTransfer/daml.yaml +15 -0
- package/src/templates/empty/daml/Main.daml +0 -22
- package/src/templates/escrow/daml/Escrow.daml +0 -53
- package/src/templates/token/daml/Token.daml +0 -56
package/README.md
CHANGED
|
@@ -60,9 +60,9 @@ Edit `src/commands/create.js` line 43:
|
|
|
60
60
|
|
|
61
61
|
```javascript
|
|
62
62
|
choices: [
|
|
63
|
-
{ name: '
|
|
63
|
+
{ name: 'Bond Trading', value: 'bondtrading' },
|
|
64
64
|
|
|
65
|
-
{ name: '
|
|
65
|
+
{ name: 'Collateral Master', value: 'collateral' },
|
|
66
66
|
|
|
67
67
|
{ name: 'My Template', value: 'my-template' }, // ADD THIS
|
|
68
68
|
|
package/package.json
CHANGED
package/src/commands/create.js
CHANGED
|
@@ -139,9 +139,9 @@ async function create(projectName, options) {
|
|
|
139
139
|
name: 'template',
|
|
140
140
|
message: 'Which template would you like to use?',
|
|
141
141
|
choices: [
|
|
142
|
-
{ name: 'Token
|
|
143
|
-
{ name: '
|
|
144
|
-
{ name: '
|
|
142
|
+
{ name: 'Token Transfer System Template', value: 'TokenTransfer' },
|
|
143
|
+
{ name: 'Multi-Party Agreement System Template', value: 'Multiparty' },
|
|
144
|
+
{ name: 'Asset Holding System Template', value: 'AssetOwner' }
|
|
145
145
|
]
|
|
146
146
|
}
|
|
147
147
|
]);
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
-- | Asset Holding System for Canton Network
|
|
2
|
+
-- CIP-0056 Token Standard Compatible
|
|
3
|
+
-- UTXO-style holdings with memo support for exchange deposits
|
|
4
|
+
module AssetHolding where
|
|
5
|
+
|
|
6
|
+
import DA.TextMap (TextMap)
|
|
7
|
+
import qualified DA.TextMap as TM
|
|
8
|
+
import Daml.Script
|
|
9
|
+
|
|
10
|
+
-------------------------------------------------------------------------------
|
|
11
|
+
-- DATA TYPES
|
|
12
|
+
-------------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
data InstrumentId = InstrumentId
|
|
15
|
+
with
|
|
16
|
+
admin : Party
|
|
17
|
+
id : Text
|
|
18
|
+
deriving (Eq, Show)
|
|
19
|
+
|
|
20
|
+
data AssetLock = AssetLock
|
|
21
|
+
with
|
|
22
|
+
lockHolder : Party
|
|
23
|
+
lockReason : Text
|
|
24
|
+
lockedUntil : Optional Time
|
|
25
|
+
deriving (Eq, Show)
|
|
26
|
+
|
|
27
|
+
data Metadata = Metadata
|
|
28
|
+
with
|
|
29
|
+
values : TextMap Text
|
|
30
|
+
deriving (Eq, Show)
|
|
31
|
+
|
|
32
|
+
-------------------------------------------------------------------------------
|
|
33
|
+
-- METADATA HELPERS
|
|
34
|
+
-------------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
createMemo : Text -> Metadata
|
|
37
|
+
createMemo memo = Metadata with
|
|
38
|
+
values = TM.fromList [("splice.lfdecentralizedtrust.org/reason", memo)]
|
|
39
|
+
|
|
40
|
+
getMemo : Metadata -> Optional Text
|
|
41
|
+
getMemo (Metadata values) = TM.lookup "splice.lfdecentralizedtrust.org/reason" values
|
|
42
|
+
|
|
43
|
+
emptyMetadata : Metadata
|
|
44
|
+
emptyMetadata = Metadata with values = TM.empty
|
|
45
|
+
|
|
46
|
+
-------------------------------------------------------------------------------
|
|
47
|
+
-- MAIN ASSET CONTRACT
|
|
48
|
+
-------------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
template Asset
|
|
51
|
+
with
|
|
52
|
+
issuer : Party
|
|
53
|
+
owner : Party
|
|
54
|
+
amount : Decimal
|
|
55
|
+
symbol : Text
|
|
56
|
+
instrumentId : InstrumentId
|
|
57
|
+
lock : Optional AssetLock
|
|
58
|
+
meta : Metadata
|
|
59
|
+
where
|
|
60
|
+
signatory issuer, owner
|
|
61
|
+
observer (case lock of
|
|
62
|
+
Some l -> [l.lockHolder]
|
|
63
|
+
None -> [])
|
|
64
|
+
|
|
65
|
+
ensure
|
|
66
|
+
amount > 0.0 &&
|
|
67
|
+
instrumentId.admin == issuer
|
|
68
|
+
|
|
69
|
+
choice Transfer : ContractId Asset
|
|
70
|
+
with
|
|
71
|
+
newOwner : Party
|
|
72
|
+
transferMeta : Metadata
|
|
73
|
+
controller owner
|
|
74
|
+
do
|
|
75
|
+
assertMsg "Cannot transfer locked asset" (isNone lock)
|
|
76
|
+
assertMsg "Cannot transfer to self" (newOwner /= owner)
|
|
77
|
+
create this with
|
|
78
|
+
owner = newOwner
|
|
79
|
+
meta = transferMeta
|
|
80
|
+
|
|
81
|
+
choice ProposeTransfer : ContractId TransferOffer
|
|
82
|
+
with
|
|
83
|
+
newOwner : Party
|
|
84
|
+
offerMeta : Metadata
|
|
85
|
+
expiresAt : Time
|
|
86
|
+
controller owner
|
|
87
|
+
do
|
|
88
|
+
assertMsg "Cannot propose transfer of locked asset" (isNone lock)
|
|
89
|
+
now <- getTime
|
|
90
|
+
assertMsg "Expiry must be in future" (expiresAt > now)
|
|
91
|
+
|
|
92
|
+
create TransferOffer with
|
|
93
|
+
assetCid = self
|
|
94
|
+
asset = this
|
|
95
|
+
sender = owner
|
|
96
|
+
receiver = newOwner
|
|
97
|
+
offerMetadata = offerMeta
|
|
98
|
+
createdAt = now
|
|
99
|
+
expiresAt = expiresAt
|
|
100
|
+
|
|
101
|
+
choice Split : (ContractId Asset, ContractId Asset)
|
|
102
|
+
with
|
|
103
|
+
splitAmount : Decimal
|
|
104
|
+
controller owner
|
|
105
|
+
do
|
|
106
|
+
assertMsg "Split amount must be positive" (splitAmount > 0.0)
|
|
107
|
+
assertMsg "Insufficient amount for split" (amount > splitAmount)
|
|
108
|
+
assertMsg "Cannot split locked asset" (isNone lock)
|
|
109
|
+
|
|
110
|
+
asset1 <- create this with
|
|
111
|
+
amount = splitAmount
|
|
112
|
+
meta = createMemo "Split from parent holding"
|
|
113
|
+
|
|
114
|
+
asset2 <- create this with
|
|
115
|
+
amount = amount - splitAmount
|
|
116
|
+
meta = createMemo "Split from parent holding"
|
|
117
|
+
|
|
118
|
+
return (asset1, asset2)
|
|
119
|
+
|
|
120
|
+
choice Merge : ContractId Asset
|
|
121
|
+
with
|
|
122
|
+
otherAssetCid : ContractId Asset
|
|
123
|
+
controller owner
|
|
124
|
+
do
|
|
125
|
+
other <- fetch otherAssetCid
|
|
126
|
+
|
|
127
|
+
assertMsg "Issuers must match" (other.issuer == issuer)
|
|
128
|
+
assertMsg "Symbols must match" (other.symbol == symbol)
|
|
129
|
+
assertMsg "Owners must match" (other.owner == owner)
|
|
130
|
+
assertMsg "InstrumentIds must match" (other.instrumentId == instrumentId)
|
|
131
|
+
assertMsg "Cannot merge locked assets" (isNone lock && isNone other.lock)
|
|
132
|
+
|
|
133
|
+
exercise otherAssetCid Archive
|
|
134
|
+
|
|
135
|
+
create this with
|
|
136
|
+
amount = amount + other.amount
|
|
137
|
+
meta = createMemo "Merged from multiple holdings"
|
|
138
|
+
|
|
139
|
+
choice LockAsset : ContractId Asset
|
|
140
|
+
with
|
|
141
|
+
lockData : AssetLock
|
|
142
|
+
controller owner
|
|
143
|
+
do
|
|
144
|
+
assertMsg "Asset is already locked" (isNone lock)
|
|
145
|
+
create this with lock = Some lockData
|
|
146
|
+
|
|
147
|
+
choice UnlockAsset : ContractId Asset
|
|
148
|
+
with
|
|
149
|
+
unlockTime : Time
|
|
150
|
+
lockHolder : Party
|
|
151
|
+
controller lockHolder
|
|
152
|
+
do
|
|
153
|
+
assertMsg "Asset is not locked" (isSome lock)
|
|
154
|
+
|
|
155
|
+
case lock of
|
|
156
|
+
Some l -> do
|
|
157
|
+
assertMsg "Not the lock holder" (l.lockHolder == lockHolder)
|
|
158
|
+
case l.lockedUntil of
|
|
159
|
+
Some until -> assertMsg "Lock period not expired" (unlockTime >= until)
|
|
160
|
+
None -> return ()
|
|
161
|
+
None -> abort "Asset is not locked"
|
|
162
|
+
|
|
163
|
+
create this with lock = None
|
|
164
|
+
|
|
165
|
+
choice Burn : ()
|
|
166
|
+
controller owner
|
|
167
|
+
do
|
|
168
|
+
assertMsg "Cannot burn locked asset" (isNone lock)
|
|
169
|
+
return ()
|
|
170
|
+
|
|
171
|
+
template TransferOffer
|
|
172
|
+
with
|
|
173
|
+
assetCid : ContractId Asset
|
|
174
|
+
asset : Asset
|
|
175
|
+
sender : Party
|
|
176
|
+
receiver : Party
|
|
177
|
+
offerMetadata : Metadata
|
|
178
|
+
createdAt : Time
|
|
179
|
+
expiresAt : Time
|
|
180
|
+
where
|
|
181
|
+
signatory sender
|
|
182
|
+
observer receiver
|
|
183
|
+
|
|
184
|
+
choice AcceptTransfer : ContractId Asset
|
|
185
|
+
controller receiver
|
|
186
|
+
do
|
|
187
|
+
now <- getTime
|
|
188
|
+
assertMsg "Transfer offer has expired" (now <= expiresAt)
|
|
189
|
+
|
|
190
|
+
exercise assetCid Archive
|
|
191
|
+
|
|
192
|
+
create asset with
|
|
193
|
+
owner = receiver
|
|
194
|
+
meta = offerMetadata
|
|
195
|
+
|
|
196
|
+
choice RejectTransfer : ContractId Asset
|
|
197
|
+
with
|
|
198
|
+
rejectionReason : Text
|
|
199
|
+
controller receiver
|
|
200
|
+
do
|
|
201
|
+
return assetCid
|
|
202
|
+
|
|
203
|
+
choice WithdrawOffer : ContractId Asset
|
|
204
|
+
controller sender
|
|
205
|
+
do
|
|
206
|
+
return assetCid
|
|
207
|
+
|
|
208
|
+
-------------------------------------------------------------------------------
|
|
209
|
+
-- EXCHANGE DEPOSIT TEMPLATE
|
|
210
|
+
-------------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
template ExchangeDepositAddress
|
|
213
|
+
with
|
|
214
|
+
exchange : Party
|
|
215
|
+
exchangeVault : Party
|
|
216
|
+
depositMemo : Text
|
|
217
|
+
supportedInstruments : [InstrumentId]
|
|
218
|
+
where
|
|
219
|
+
signatory exchange
|
|
220
|
+
observer exchangeVault
|
|
221
|
+
|
|
222
|
+
nonconsuming choice DepositToExchange : ContractId Asset
|
|
223
|
+
with
|
|
224
|
+
userAssetCid : ContractId Asset
|
|
225
|
+
user : Party
|
|
226
|
+
controller user, exchange
|
|
227
|
+
do
|
|
228
|
+
asset <- fetch userAssetCid
|
|
229
|
+
|
|
230
|
+
assertMsg "Instrument not supported"
|
|
231
|
+
(asset.instrumentId `elem` supportedInstruments)
|
|
232
|
+
|
|
233
|
+
exercise userAssetCid Transfer with
|
|
234
|
+
newOwner = exchangeVault
|
|
235
|
+
transferMeta = createMemo depositMemo
|
|
236
|
+
|
|
237
|
+
-------------------------------------------------------------------------------
|
|
238
|
+
-- HELPER FUNCTIONS
|
|
239
|
+
-------------------------------------------------------------------------------
|
|
240
|
+
|
|
241
|
+
isNone : Optional a -> Bool
|
|
242
|
+
isNone opt = case opt of
|
|
243
|
+
None -> True
|
|
244
|
+
Some _ -> False
|
|
245
|
+
|
|
246
|
+
isSome : Optional a -> Bool
|
|
247
|
+
isSome opt = not (isNone opt)
|
|
248
|
+
|
|
249
|
+
-------------------------------------------------------------------------------
|
|
250
|
+
-- TEST SCRIPTS
|
|
251
|
+
-------------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
setup : Script ()
|
|
254
|
+
setup = do
|
|
255
|
+
issuer <- allocateParty "AssetIssuer"
|
|
256
|
+
alice <- allocateParty "Alice"
|
|
257
|
+
bob <- allocateParty "Bob"
|
|
258
|
+
|
|
259
|
+
let instrumentId = InstrumentId with
|
|
260
|
+
admin = issuer
|
|
261
|
+
id = "GOLD-TOKEN"
|
|
262
|
+
|
|
263
|
+
assetCid <- submitMulti [issuer, alice] [] $ createCmd Asset with
|
|
264
|
+
issuer = issuer
|
|
265
|
+
owner = alice
|
|
266
|
+
amount = 1000.0
|
|
267
|
+
symbol = "GOLD"
|
|
268
|
+
instrumentId = instrumentId
|
|
269
|
+
lock = None
|
|
270
|
+
meta = createMemo "Initial issuance"
|
|
271
|
+
|
|
272
|
+
submitMulti [alice, issuer] [] $ exerciseCmd assetCid Transfer with
|
|
273
|
+
newOwner = bob
|
|
274
|
+
transferMeta = createMemo "Payment to Bob"
|
|
275
|
+
|
|
276
|
+
return ()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Asset Ownership System for Canton Network
|
|
2
|
+
# CIP-0056 Token Standard Compatible
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
sdk-version: 3.4.9
|
|
6
|
+
name: AssetOwnership
|
|
7
|
+
source: daml/AssetHolding.daml
|
|
8
|
+
parties:
|
|
9
|
+
- AssetIssuer
|
|
10
|
+
- Alice
|
|
11
|
+
- Bob
|
|
12
|
+
- Exchange
|
|
13
|
+
- ExchangeVault
|
|
14
|
+
version: 1.0.0
|
|
15
|
+
dependencies:
|
|
16
|
+
- daml-prim
|
|
17
|
+
- daml-stdlib
|
|
18
|
+
- daml-script
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
-- | Multi-Party Agreement System for Canton Network
|
|
2
|
+
-- Supports incremental signing, governance, multi-sig workflows
|
|
3
|
+
module MultiPartyAgreement where
|
|
4
|
+
|
|
5
|
+
import DA.List (delete, sort)
|
|
6
|
+
import Daml.Script
|
|
7
|
+
|
|
8
|
+
-------------------------------------------------------------------------------
|
|
9
|
+
-- DATA TYPES
|
|
10
|
+
-------------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
data AgreementType
|
|
13
|
+
= Governance
|
|
14
|
+
| Transaction
|
|
15
|
+
| Legal
|
|
16
|
+
| Policy
|
|
17
|
+
| Custom Text
|
|
18
|
+
deriving (Eq, Show)
|
|
19
|
+
|
|
20
|
+
data AgreementMetadata = AgreementMetadata
|
|
21
|
+
with
|
|
22
|
+
agreementType : AgreementType
|
|
23
|
+
createdAt : Time
|
|
24
|
+
description : Text
|
|
25
|
+
attachmentHashes : [Text]
|
|
26
|
+
deriving (Eq, Show)
|
|
27
|
+
|
|
28
|
+
data Signature = Signature
|
|
29
|
+
with
|
|
30
|
+
signer : Party
|
|
31
|
+
signedAt : Time
|
|
32
|
+
comments : Optional Text
|
|
33
|
+
deriving (Eq, Show)
|
|
34
|
+
|
|
35
|
+
-------------------------------------------------------------------------------
|
|
36
|
+
-- FINALIZED AGREEMENT
|
|
37
|
+
-------------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
template Agreement
|
|
40
|
+
with
|
|
41
|
+
signatories : [Party]
|
|
42
|
+
terms : Text
|
|
43
|
+
agreementId : Text
|
|
44
|
+
metadata : AgreementMetadata
|
|
45
|
+
signatures : [Signature]
|
|
46
|
+
finalizedAt : Time
|
|
47
|
+
where
|
|
48
|
+
signatory signatories
|
|
49
|
+
|
|
50
|
+
ensure uniqueParties signatories
|
|
51
|
+
|
|
52
|
+
choice ArchiveAgreement : ()
|
|
53
|
+
controller signatories
|
|
54
|
+
do
|
|
55
|
+
return ()
|
|
56
|
+
|
|
57
|
+
choice ProposeAmendment : ContractId AgreementProposal
|
|
58
|
+
with
|
|
59
|
+
proposer : Party
|
|
60
|
+
newTerms : Text
|
|
61
|
+
amendmentReason : Text
|
|
62
|
+
controller proposer
|
|
63
|
+
do
|
|
64
|
+
assertMsg "Proposer must be a signatory" (proposer `elem` signatories)
|
|
65
|
+
now <- getTime
|
|
66
|
+
create AgreementProposal with
|
|
67
|
+
proposer = proposer
|
|
68
|
+
allParties = signatories
|
|
69
|
+
alreadySigned = [proposer]
|
|
70
|
+
pendingParties = delete proposer signatories
|
|
71
|
+
terms = newTerms
|
|
72
|
+
agreementId = agreementId <> "-amendment"
|
|
73
|
+
metadata = metadata with
|
|
74
|
+
description = amendmentReason
|
|
75
|
+
createdAt = now
|
|
76
|
+
signatures = [Signature with
|
|
77
|
+
signer = proposer
|
|
78
|
+
signedAt = now
|
|
79
|
+
comments = Some amendmentReason]
|
|
80
|
+
requiredSignatures = length signatories
|
|
81
|
+
expiresAt = None
|
|
82
|
+
|
|
83
|
+
-------------------------------------------------------------------------------
|
|
84
|
+
-- AGREEMENT PROPOSAL
|
|
85
|
+
-------------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
template AgreementProposal
|
|
88
|
+
with
|
|
89
|
+
proposer : Party
|
|
90
|
+
allParties : [Party]
|
|
91
|
+
alreadySigned : [Party]
|
|
92
|
+
pendingParties : [Party]
|
|
93
|
+
terms : Text
|
|
94
|
+
agreementId : Text
|
|
95
|
+
metadata : AgreementMetadata
|
|
96
|
+
signatures : [Signature]
|
|
97
|
+
requiredSignatures : Int
|
|
98
|
+
expiresAt : Optional Time
|
|
99
|
+
where
|
|
100
|
+
signatory alreadySigned
|
|
101
|
+
observer allParties
|
|
102
|
+
|
|
103
|
+
ensure
|
|
104
|
+
uniqueParties allParties &&
|
|
105
|
+
uniqueParties alreadySigned &&
|
|
106
|
+
requiredSignatures > 0 &&
|
|
107
|
+
requiredSignatures <= length allParties &&
|
|
108
|
+
length alreadySigned <= requiredSignatures
|
|
109
|
+
|
|
110
|
+
choice Sign : ContractId AgreementProposal
|
|
111
|
+
with
|
|
112
|
+
signer : Party
|
|
113
|
+
signatureComments : Optional Text
|
|
114
|
+
controller signer
|
|
115
|
+
do
|
|
116
|
+
now <- getTime
|
|
117
|
+
|
|
118
|
+
assertMsg "Party is not in required signatories" (signer `elem` allParties)
|
|
119
|
+
assertMsg "Party has already signed" (signer `notElem` alreadySigned)
|
|
120
|
+
assertMsg "Party is not in pending list" (signer `elem` pendingParties)
|
|
121
|
+
|
|
122
|
+
case expiresAt of
|
|
123
|
+
Some expiry -> assertMsg "Proposal has expired" (now <= expiry)
|
|
124
|
+
None -> return ()
|
|
125
|
+
|
|
126
|
+
let newSignature = Signature with
|
|
127
|
+
signer = signer
|
|
128
|
+
signedAt = now
|
|
129
|
+
comments = signatureComments
|
|
130
|
+
|
|
131
|
+
create this with
|
|
132
|
+
alreadySigned = signer :: alreadySigned
|
|
133
|
+
pendingParties = delete signer pendingParties
|
|
134
|
+
signatures = newSignature :: signatures
|
|
135
|
+
|
|
136
|
+
choice Finalize : ContractId Agreement
|
|
137
|
+
controller proposer
|
|
138
|
+
do
|
|
139
|
+
now <- getTime
|
|
140
|
+
assertMsg "Not enough signatures"
|
|
141
|
+
(length alreadySigned >= requiredSignatures)
|
|
142
|
+
|
|
143
|
+
case expiresAt of
|
|
144
|
+
Some expiry -> assertMsg "Cannot finalize expired proposal" (now <= expiry)
|
|
145
|
+
None -> return ()
|
|
146
|
+
|
|
147
|
+
create Agreement with
|
|
148
|
+
signatories = sort alreadySigned
|
|
149
|
+
terms = terms
|
|
150
|
+
agreementId = agreementId
|
|
151
|
+
metadata = metadata
|
|
152
|
+
signatures = reverse signatures
|
|
153
|
+
finalizedAt = now
|
|
154
|
+
|
|
155
|
+
choice Reject : ()
|
|
156
|
+
with
|
|
157
|
+
rejector : Party
|
|
158
|
+
reason : Text
|
|
159
|
+
controller rejector
|
|
160
|
+
do
|
|
161
|
+
assertMsg "Only proposer or signatories can reject"
|
|
162
|
+
(rejector == proposer || rejector `elem` allParties)
|
|
163
|
+
return ()
|
|
164
|
+
|
|
165
|
+
choice UpdateExpiration : ContractId AgreementProposal
|
|
166
|
+
with
|
|
167
|
+
newExpiry : Optional Time
|
|
168
|
+
controller proposer
|
|
169
|
+
do
|
|
170
|
+
now <- getTime
|
|
171
|
+
case newExpiry of
|
|
172
|
+
Some expiry -> assertMsg "New expiry must be in future" (expiry > now)
|
|
173
|
+
None -> return ()
|
|
174
|
+
create this with expiresAt = newExpiry
|
|
175
|
+
|
|
176
|
+
choice AddRequiredParty : ContractId AgreementProposal
|
|
177
|
+
with
|
|
178
|
+
newParty : Party
|
|
179
|
+
controller proposer
|
|
180
|
+
do
|
|
181
|
+
assertMsg "Party is already required" (newParty `notElem` allParties)
|
|
182
|
+
create this with
|
|
183
|
+
allParties = newParty :: allParties
|
|
184
|
+
pendingParties = newParty :: pendingParties
|
|
185
|
+
requiredSignatures = requiredSignatures + 1
|
|
186
|
+
|
|
187
|
+
choice RemoveRequiredParty : ContractId AgreementProposal
|
|
188
|
+
with
|
|
189
|
+
partyToRemove : Party
|
|
190
|
+
controller proposer
|
|
191
|
+
do
|
|
192
|
+
assertMsg "Cannot remove party who has signed"
|
|
193
|
+
(partyToRemove `notElem` alreadySigned)
|
|
194
|
+
assertMsg "Party is not in required list"
|
|
195
|
+
(partyToRemove `elem` allParties)
|
|
196
|
+
|
|
197
|
+
create this with
|
|
198
|
+
allParties = delete partyToRemove allParties
|
|
199
|
+
pendingParties = delete partyToRemove pendingParties
|
|
200
|
+
requiredSignatures = requiredSignatures - 1
|
|
201
|
+
|
|
202
|
+
-------------------------------------------------------------------------------
|
|
203
|
+
-- HELPER FUNCTIONS
|
|
204
|
+
-------------------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
uniqueParties : [Party] -> Bool
|
|
207
|
+
uniqueParties parties = length parties == length (dedup parties)
|
|
208
|
+
|
|
209
|
+
dedup : Eq a => [a] -> [a]
|
|
210
|
+
dedup [] = []
|
|
211
|
+
dedup (x::xs) = x :: dedup (filter (/= x) xs)
|
|
212
|
+
|
|
213
|
+
-------------------------------------------------------------------------------
|
|
214
|
+
-- TEST SCRIPTS
|
|
215
|
+
-------------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
setup : Script ()
|
|
218
|
+
setup = do
|
|
219
|
+
alice <- allocateParty "Alice"
|
|
220
|
+
bob <- allocateParty "Bob"
|
|
221
|
+
charlie <- allocateParty "Charlie"
|
|
222
|
+
|
|
223
|
+
now <- getTime
|
|
224
|
+
|
|
225
|
+
let meta = AgreementMetadata with
|
|
226
|
+
agreementType = Governance
|
|
227
|
+
createdAt = now
|
|
228
|
+
description = "Partnership agreement"
|
|
229
|
+
attachmentHashes = []
|
|
230
|
+
|
|
231
|
+
proposalCid <- submit alice $ createCmd AgreementProposal with
|
|
232
|
+
proposer = alice
|
|
233
|
+
allParties = [alice, bob, charlie]
|
|
234
|
+
alreadySigned = [alice]
|
|
235
|
+
pendingParties = [bob, charlie]
|
|
236
|
+
terms = "All parties agree to cooperate on the project"
|
|
237
|
+
agreementId = "AGREEMENT-001"
|
|
238
|
+
metadata = meta
|
|
239
|
+
signatures = [Signature with
|
|
240
|
+
signer = alice
|
|
241
|
+
signedAt = now
|
|
242
|
+
comments = Some "Initiated by Alice"]
|
|
243
|
+
requiredSignatures = 3
|
|
244
|
+
expiresAt = None
|
|
245
|
+
|
|
246
|
+
proposalCid2 <- submit bob $ exerciseCmd proposalCid Sign with
|
|
247
|
+
signer = bob
|
|
248
|
+
signatureComments = Some "Approved by Bob"
|
|
249
|
+
|
|
250
|
+
proposalCid3 <- submit charlie $ exerciseCmd proposalCid2 Sign with
|
|
251
|
+
signer = charlie
|
|
252
|
+
signatureComments = Some "Approved by Charlie"
|
|
253
|
+
|
|
254
|
+
submit alice $ exerciseCmd proposalCid3 Finalize
|
|
255
|
+
|
|
256
|
+
return ()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Multi-Party Agreement System for Canton Network
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
sdk-version: 3.4.9
|
|
5
|
+
name: MultiPartyAgreement
|
|
6
|
+
source: daml/MultiPartyAgreement.daml
|
|
7
|
+
parties:
|
|
8
|
+
- Alice
|
|
9
|
+
- Bob
|
|
10
|
+
- Charlie
|
|
11
|
+
version: 1.0.0
|
|
12
|
+
dependencies:
|
|
13
|
+
- daml-prim
|
|
14
|
+
- daml-stdlib
|
|
15
|
+
- daml-script
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
-- | Token Transfer System for Canton Network
|
|
2
|
+
-- Supports 1-step and 2-step transfers, split/merge, locking
|
|
3
|
+
module Token where
|
|
4
|
+
|
|
5
|
+
import Daml.Script
|
|
6
|
+
|
|
7
|
+
-------------------------------------------------------------------------------
|
|
8
|
+
-- DATA TYPES
|
|
9
|
+
-------------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
data TransferMetadata = TransferMetadata
|
|
12
|
+
with
|
|
13
|
+
reason : Optional Text
|
|
14
|
+
reference : Optional Text
|
|
15
|
+
timestamp : Time
|
|
16
|
+
deriving (Eq, Show)
|
|
17
|
+
|
|
18
|
+
data TokenLock = TokenLock
|
|
19
|
+
with
|
|
20
|
+
lockHolder : Party
|
|
21
|
+
unlockCondition : Text
|
|
22
|
+
expiresAt : Optional Time
|
|
23
|
+
deriving (Eq, Show)
|
|
24
|
+
|
|
25
|
+
-------------------------------------------------------------------------------
|
|
26
|
+
-- MAIN TOKEN CONTRACT
|
|
27
|
+
-------------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
template Token
|
|
30
|
+
with
|
|
31
|
+
issuer : Party
|
|
32
|
+
owner : Party
|
|
33
|
+
amount : Decimal
|
|
34
|
+
tokenId : Text
|
|
35
|
+
symbol : Text
|
|
36
|
+
lock : Optional TokenLock
|
|
37
|
+
metadata : TransferMetadata
|
|
38
|
+
where
|
|
39
|
+
signatory issuer, owner
|
|
40
|
+
observer (case lock of
|
|
41
|
+
Some l -> [l.lockHolder]
|
|
42
|
+
None -> [])
|
|
43
|
+
|
|
44
|
+
ensure amount > 0.0
|
|
45
|
+
|
|
46
|
+
choice Transfer : ContractId Token
|
|
47
|
+
with
|
|
48
|
+
newOwner : Party
|
|
49
|
+
newMetadata : TransferMetadata
|
|
50
|
+
controller owner
|
|
51
|
+
do
|
|
52
|
+
assertMsg "Cannot transfer locked tokens" (isNone lock)
|
|
53
|
+
assertMsg "Cannot transfer to self" (newOwner /= owner)
|
|
54
|
+
create this with
|
|
55
|
+
owner = newOwner
|
|
56
|
+
metadata = newMetadata
|
|
57
|
+
|
|
58
|
+
choice ProposeTransfer : ContractId TransferProposal
|
|
59
|
+
with
|
|
60
|
+
newOwner : Party
|
|
61
|
+
proposalMetadata : TransferMetadata
|
|
62
|
+
expiresAt : Time
|
|
63
|
+
controller owner
|
|
64
|
+
do
|
|
65
|
+
assertMsg "Cannot propose transfer of locked tokens" (isNone lock)
|
|
66
|
+
assertMsg "Expiry must be in the future" (expiresAt > proposalMetadata.timestamp)
|
|
67
|
+
create TransferProposal with
|
|
68
|
+
tokenCid = self
|
|
69
|
+
currentOwner = owner
|
|
70
|
+
proposedOwner = newOwner
|
|
71
|
+
token = this
|
|
72
|
+
metadata = proposalMetadata
|
|
73
|
+
expiresAt = expiresAt
|
|
74
|
+
|
|
75
|
+
choice Split : (ContractId Token, ContractId Token)
|
|
76
|
+
with
|
|
77
|
+
splitAmount : Decimal
|
|
78
|
+
splitTime : Time
|
|
79
|
+
controller owner
|
|
80
|
+
do
|
|
81
|
+
assertMsg "Split amount must be positive" (splitAmount > 0.0)
|
|
82
|
+
assertMsg "Split amount must be less than total" (splitAmount < amount)
|
|
83
|
+
assertMsg "Cannot split locked tokens" (isNone lock)
|
|
84
|
+
|
|
85
|
+
let newMetadata = TransferMetadata with
|
|
86
|
+
reason = Some "Token split"
|
|
87
|
+
reference = metadata.reference
|
|
88
|
+
timestamp = splitTime
|
|
89
|
+
|
|
90
|
+
t1 <- create this with
|
|
91
|
+
amount = splitAmount
|
|
92
|
+
metadata = newMetadata
|
|
93
|
+
t2 <- create this with
|
|
94
|
+
amount = amount - splitAmount
|
|
95
|
+
metadata = newMetadata
|
|
96
|
+
|
|
97
|
+
return (t1, t2)
|
|
98
|
+
|
|
99
|
+
choice MergeToken : ContractId Token
|
|
100
|
+
with
|
|
101
|
+
otherTokenCid : ContractId Token
|
|
102
|
+
mergeTime : Time
|
|
103
|
+
controller owner
|
|
104
|
+
do
|
|
105
|
+
other <- fetch otherTokenCid
|
|
106
|
+
assertMsg "Issuers must match" (other.issuer == issuer)
|
|
107
|
+
assertMsg "Token IDs must match" (other.tokenId == tokenId)
|
|
108
|
+
assertMsg "Owners must match" (other.owner == owner)
|
|
109
|
+
assertMsg "Cannot merge locked tokens" (isNone lock && isNone other.lock)
|
|
110
|
+
|
|
111
|
+
exercise otherTokenCid Archive
|
|
112
|
+
|
|
113
|
+
create this with
|
|
114
|
+
amount = amount + other.amount
|
|
115
|
+
metadata = TransferMetadata with
|
|
116
|
+
reason = Some "Token merge"
|
|
117
|
+
reference = metadata.reference
|
|
118
|
+
timestamp = mergeTime
|
|
119
|
+
|
|
120
|
+
choice LockTokens : ContractId Token
|
|
121
|
+
with
|
|
122
|
+
lockData : TokenLock
|
|
123
|
+
controller owner
|
|
124
|
+
do
|
|
125
|
+
assertMsg "Token is already locked" (isNone lock)
|
|
126
|
+
create this with lock = Some lockData
|
|
127
|
+
|
|
128
|
+
choice UnlockTokens : ContractId Token
|
|
129
|
+
with
|
|
130
|
+
lockHolder : Party
|
|
131
|
+
controller lockHolder
|
|
132
|
+
do
|
|
133
|
+
assertMsg "Token is not locked" (isSome lock)
|
|
134
|
+
case lock of
|
|
135
|
+
Some l -> assertMsg "Not the lock holder" (l.lockHolder == lockHolder)
|
|
136
|
+
None -> abort "Token is not locked"
|
|
137
|
+
create this with lock = None
|
|
138
|
+
|
|
139
|
+
choice Burn : ()
|
|
140
|
+
controller owner
|
|
141
|
+
do
|
|
142
|
+
assertMsg "Cannot burn locked tokens" (isNone lock)
|
|
143
|
+
return ()
|
|
144
|
+
|
|
145
|
+
template TransferProposal
|
|
146
|
+
with
|
|
147
|
+
tokenCid : ContractId Token
|
|
148
|
+
currentOwner : Party
|
|
149
|
+
proposedOwner : Party
|
|
150
|
+
token : Token
|
|
151
|
+
metadata : TransferMetadata
|
|
152
|
+
expiresAt : Time
|
|
153
|
+
where
|
|
154
|
+
signatory currentOwner
|
|
155
|
+
observer proposedOwner
|
|
156
|
+
|
|
157
|
+
choice AcceptTransfer : ContractId Token
|
|
158
|
+
with
|
|
159
|
+
acceptTime : Time
|
|
160
|
+
controller proposedOwner
|
|
161
|
+
do
|
|
162
|
+
assertMsg "Transfer proposal has expired" (acceptTime <= expiresAt)
|
|
163
|
+
exercise tokenCid Archive
|
|
164
|
+
create token with
|
|
165
|
+
owner = proposedOwner
|
|
166
|
+
metadata = metadata with timestamp = acceptTime
|
|
167
|
+
|
|
168
|
+
choice RejectTransfer : ContractId Token
|
|
169
|
+
controller proposedOwner
|
|
170
|
+
do
|
|
171
|
+
return tokenCid
|
|
172
|
+
|
|
173
|
+
choice WithdrawProposal : ContractId Token
|
|
174
|
+
controller currentOwner
|
|
175
|
+
do
|
|
176
|
+
return tokenCid
|
|
177
|
+
|
|
178
|
+
-------------------------------------------------------------------------------
|
|
179
|
+
-- HELPER FUNCTIONS
|
|
180
|
+
-------------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
isNone : Optional a -> Bool
|
|
183
|
+
isNone opt = case opt of
|
|
184
|
+
None -> True
|
|
185
|
+
Some _ -> False
|
|
186
|
+
|
|
187
|
+
isSome : Optional a -> Bool
|
|
188
|
+
isSome opt = not (isNone opt)
|
|
189
|
+
|
|
190
|
+
-------------------------------------------------------------------------------
|
|
191
|
+
-- TEST SCRIPTS
|
|
192
|
+
-------------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
setup : Script ()
|
|
195
|
+
setup = do
|
|
196
|
+
alice <- allocateParty "Alice"
|
|
197
|
+
bob <- allocateParty "Bob"
|
|
198
|
+
issuer <- allocateParty "TokenIssuer"
|
|
199
|
+
|
|
200
|
+
now <- getTime
|
|
201
|
+
|
|
202
|
+
let transferMeta = TransferMetadata with
|
|
203
|
+
reason = Some "Initial allocation"
|
|
204
|
+
reference = None
|
|
205
|
+
timestamp = now
|
|
206
|
+
|
|
207
|
+
tokenCid <- submitMulti [issuer, alice] [] $ createCmd Token with
|
|
208
|
+
issuer = issuer
|
|
209
|
+
owner = alice
|
|
210
|
+
amount = 1000.0
|
|
211
|
+
tokenId = "TEST-TOKEN"
|
|
212
|
+
symbol = "TEST"
|
|
213
|
+
lock = None
|
|
214
|
+
metadata = transferMeta
|
|
215
|
+
|
|
216
|
+
submitMulti [alice, issuer] [] $ exerciseCmd tokenCid Transfer with
|
|
217
|
+
newOwner = bob
|
|
218
|
+
newMetadata = transferMeta with
|
|
219
|
+
reason = Some "Payment to Bob"
|
|
220
|
+
timestamp = now
|
|
221
|
+
|
|
222
|
+
return ()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Token Transfer System for Canton Network
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
sdk-version: 3.4.9
|
|
5
|
+
name: TokenTransfer
|
|
6
|
+
source: daml/Token.daml
|
|
7
|
+
parties:
|
|
8
|
+
- TokenIssuer
|
|
9
|
+
- Alice
|
|
10
|
+
- Bob
|
|
11
|
+
version: 1.0.0
|
|
12
|
+
dependencies:
|
|
13
|
+
- daml-prim
|
|
14
|
+
- daml-stdlib
|
|
15
|
+
- daml-script
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
-- Main.daml
|
|
2
|
-
-- Your Canton smart contract starts here!
|
|
3
|
-
|
|
4
|
-
module Main where
|
|
5
|
-
|
|
6
|
-
import Daml.Script
|
|
7
|
-
|
|
8
|
-
template MyContract
|
|
9
|
-
with
|
|
10
|
-
party1 : Party
|
|
11
|
-
where
|
|
12
|
-
signatory party1
|
|
13
|
-
|
|
14
|
-
setup : Script ()
|
|
15
|
-
setup = script do
|
|
16
|
-
alice <- allocateParty "Alice"
|
|
17
|
-
|
|
18
|
-
submit alice do
|
|
19
|
-
createCmd MyContract with
|
|
20
|
-
party1 = alice
|
|
21
|
-
|
|
22
|
-
return ()
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
-- Escrow.daml
|
|
2
|
-
-- A two-party escrow with an arbiter
|
|
3
|
-
|
|
4
|
-
module Escrow where
|
|
5
|
-
|
|
6
|
-
import Daml.Script
|
|
7
|
-
|
|
8
|
-
template EscrowAgreement
|
|
9
|
-
with
|
|
10
|
-
buyer : Party
|
|
11
|
-
seller : Party
|
|
12
|
-
arbiter : Party
|
|
13
|
-
amount : Decimal
|
|
14
|
-
description : Text
|
|
15
|
-
where
|
|
16
|
-
signatory buyer, seller, arbiter
|
|
17
|
-
|
|
18
|
-
choice Release : ()
|
|
19
|
-
controller buyer
|
|
20
|
-
do
|
|
21
|
-
return ()
|
|
22
|
-
|
|
23
|
-
choice Dispute : ()
|
|
24
|
-
with
|
|
25
|
-
decision : Text
|
|
26
|
-
controller arbiter
|
|
27
|
-
do
|
|
28
|
-
return ()
|
|
29
|
-
|
|
30
|
-
choice Cancel : ()
|
|
31
|
-
controller buyer, seller
|
|
32
|
-
do
|
|
33
|
-
return ()
|
|
34
|
-
|
|
35
|
-
setup : Script ()
|
|
36
|
-
setup = script do
|
|
37
|
-
alice <- allocateParty "Alice"
|
|
38
|
-
bob <- allocateParty "Bob"
|
|
39
|
-
charlie <- allocateParty "Charlie"
|
|
40
|
-
|
|
41
|
-
escrow <- submit alice do
|
|
42
|
-
submitMulti [alice, bob, charlie] [] do
|
|
43
|
-
createCmd EscrowAgreement with
|
|
44
|
-
buyer = alice
|
|
45
|
-
seller = bob
|
|
46
|
-
arbiter = charlie
|
|
47
|
-
amount = 1000.0
|
|
48
|
-
description = "Laptop purchase"
|
|
49
|
-
|
|
50
|
-
submit alice do
|
|
51
|
-
exerciseCmd escrow Release
|
|
52
|
-
|
|
53
|
-
return ()
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
-- Token.daml
|
|
2
|
-
-- A simple fungible token on Canton Network
|
|
3
|
-
|
|
4
|
-
module Token where
|
|
5
|
-
|
|
6
|
-
import Daml.Script
|
|
7
|
-
|
|
8
|
-
-- The Token template represents a token holding
|
|
9
|
-
template Token
|
|
10
|
-
with
|
|
11
|
-
issuer : Party
|
|
12
|
-
owner : Party
|
|
13
|
-
amount : Decimal
|
|
14
|
-
symbol : Text
|
|
15
|
-
where
|
|
16
|
-
signatory issuer
|
|
17
|
-
observer owner
|
|
18
|
-
|
|
19
|
-
-- Transfer tokens to another party
|
|
20
|
-
choice Transfer : ContractId Token
|
|
21
|
-
with
|
|
22
|
-
newOwner : Party
|
|
23
|
-
controller owner
|
|
24
|
-
do
|
|
25
|
-
create this with owner = newOwner
|
|
26
|
-
|
|
27
|
-
-- Split tokens (for partial transfers)
|
|
28
|
-
choice Split : (ContractId Token, ContractId Token)
|
|
29
|
-
with
|
|
30
|
-
splitAmount : Decimal
|
|
31
|
-
controller owner
|
|
32
|
-
do
|
|
33
|
-
assertMsg "Split amount must be less than total" (splitAmount < amount)
|
|
34
|
-
token1 <- create this with amount = splitAmount
|
|
35
|
-
token2 <- create this with amount = amount - splitAmount
|
|
36
|
-
return (token1, token2)
|
|
37
|
-
|
|
38
|
-
-- Example: How to use this in practice
|
|
39
|
-
setup : Script ()
|
|
40
|
-
setup = script do
|
|
41
|
-
alice <- allocateParty "Alice"
|
|
42
|
-
bob <- allocateParty "Bob"
|
|
43
|
-
issuer <- allocateParty "TokenIssuer"
|
|
44
|
-
|
|
45
|
-
aliceToken <- submit issuer do
|
|
46
|
-
createCmd Token with
|
|
47
|
-
issuer = issuer
|
|
48
|
-
owner = alice
|
|
49
|
-
amount = 1000.0
|
|
50
|
-
symbol = "TEST"
|
|
51
|
-
|
|
52
|
-
submit alice do
|
|
53
|
-
exerciseCmd aliceToken Transfer with
|
|
54
|
-
newOwner = bob
|
|
55
|
-
|
|
56
|
-
return ()
|