The pattern you describe of separating data and rules is one that has been debated at great lengths. I think it is a very good guiding principle and helps structure a project in such a way that upgrades become easier. It does, however, also increase the complexity slightly as transferring authority and managing visibility become a bit more involved.
Let’s imagine we have Cash
signed by issuer and owner, without any choices. We now want to write a transfer rule from one owner to another, say from Alice
and Bob
. Let’s assume all “rules” (what you call services) are on a separate CashRules
contract.
Who signs the CashRules
contract? Is it bilateral between issuer and owners? If so, how does does one owner find out that another subscribes to the rules?
Does everyone sign one instance? Then all cash owners know about all others. by virtue of all being signatories on one contract. Is that acceptable?
Let’s assume cash owners should not know about each other until they do business. So Alice
starts with a CashRules
contract signed and visible only to Alice
and Issuer
.
At some point in the process, we need to get the authority of all three parties into scope. Without the separation that can be done with a TransferProposal
signed by Alice
and Issuer
with a choice for Bob
. But if we don’t allow ourselves a choice for Bob
on the data (which in this case is Cash
or TransferProposal
, that doesn’t work. So Alice
has to issue a side-contract for the CashRules
that allows Bob
to accept transfers. The issuer probably only wants Bob
to be able to use that if he, too, subscribes to the rules. All this leads to something like this:
template Cash
with
issuer : Party
owner : Party
where
signatory issuer, owner
template TransferProposal
with
issuer : Party
owner : Party
newOwner : Party
where
signatory issuer, owner
observer newOwner
template CashRules
with
issuer : Party
owner : Party
where
signatory issuer, owner
controller owner can
nonconsuming ProposeTransfer : ContractId TransferProposal
with
cash : ContractId Cash
newOwner : Party
do
c <- fetch cash
assert (c.owner == owner)
assert (c.issuer == issuer)
archive cash
let tp = TransferProposal with ..
r <- create tp
oCid <- lookupByKey @TransferRules tp
case oCid of
None -> create TransferRules with ..
Some cid -> return cid
return r
nonconsuming AcceptTransfer : ContractId Cash
with
cid : ContractId TransferProposal
do
tp <- fetch cid
exerciseByKey @TransferRules tp TR_AcceptTransfer with ..
template TransferRules
with
issuer : Party
owner : Party
newOwner : Party
where
signatory issuer, owner
key TransferProposal with .. : TransferProposal
maintainer key.issuer, key.owner
controller issuer, newOwner can
nonconsuming TR_AcceptTransfer : ContractId Cash
with
cid : ContractId TransferProposal
do
tp <- fetch cid
assert (tp == TransferProposal with ..)
archive cid
create Cash with issuer, owner = newOwner
template Setup
with
issuer : Party
owner : Party
where
signatory issuer
controller owner can
Init : (ContractId Cash, ContractId CashRules)
do
c <- create Cash with ..
cr <- create CashRules with ..
return (c, cr)
t = scenario do
[issuer, alice, bob] <- mapA getParty ["Issuer", "Alice", "Bob"]
(init1, init2) <- submit issuer do
init1 <- create Setup with owner = alice; ..
init2 <- create Setup with owner = bob; ..
return (init1, init2)
(ac, acr) <- submit alice do
exercise init1 Init
(bc, bcr) <- submit bob do
exercise init2 Init
tp <- submit alice do
exercise acr ProposeTransfer with
cash = ac
newOwner = bob
bc2 <- submit bob do
exercise bcr AcceptTransfer with
cid = tp
return ()
That’s quite involved compared to the more basic approach. I therefore personally tend to keep “basic” choices like transfers on the templates themselves, and only build higher level functionality like Swaps in separate rule templates.
EDIT: For completeness, I added a Setup
template and short scenario to demonstrate. As you can see, the final usage is just as easy as the simpler “integrated” approach, but there are twice as many templates and more involved choice bodies.