Composability is currently an Early Access Feature in Alpha status.
Note
The example in this tutorial uses unsupported Scala bindings and codegen.
Composability¶
In this tutorial, you will learn how to build workflows that span several Canton domains. Composability turns those several Canton domains into one conceptual ledger at the application level.
The tutorial assumes the following prerequisites:
- You have worked through the Getting started tutorial and know how to interact with the Canton console.
- You know the Daml concepts that are covered in the Daml introduction.
- The running example uses the ledger API, the Scala codegen (no longer supported by Daml) for Daml, and Canton’s identity management. If you want to understand the example code in full, please refer to the above documentation.
The tutorial consists of two parts:
- The first part illustrates how to design a workflow that spans multiple domains.
- The second part shows how to compose existing workflows on different domains into a single workflow and the benefits this brings.
The Daml models are shipped with the Canton release in the daml/CantonExamples
folder in the modules Iou
and Paint
.
The configuration and the steps are available in the examples/05-composability
folder of the Canton release.
To run the workflow, start Canton from the release’s root folder as follows:
./bin/canton -c examples/05-composability/composability.conf
You can copy-paste the console commands from the tutorial in the given order into the Canton console to run them interactively.
All console commands are also summarized in the bootstrap scripts composability1.canton
,
composability-auto-transfer.canton
, and composability2.canton
.
Note
Note that to use composability, we do have to turn off contract key uniqueness, as uniqueness cannot be provided across multiple domains. Therefore, composability is just a preview feature and explained here to demonstrate an early version of it that is not yet suitable for production use.
Part 1: A multi-domain workflow¶
We consider the paint agreement scenario from the Getting started tutorial. The house owner and the painter want to enter a paint agreement that obliges the painter to paint the house owner’s house. To enter such an agreement, the house owner proposes a paint offer to the painter and the painter accepts. Upon acceptance, the paint agreement shall be created atomically with changing the ownership of the money, which we represent by an IOU backed by the bank.
Atomicity guarantees that no party can scam the other: The painter enters the obligation of painting the house only if house owner pays, and the house owner pays only if the painter enters the obligation. This avoid bad scenarios such as the following, which would have to be resolved out of band, e.g., using legal processes:
- The house owner spends the IOU on something else and does not pay the painter, even though the painter has entered the obligation to paint the house. The painter then needs to convince the house owner to pay with another IOU or to revoke the paint agreement.
- The house owner wires the money to the painter, but the painter refuses to enter the paint agreement. The house owner then begs the painter to return the money.
Setting up the topology¶
In this example, we assume a topology with two domains, iou
and paint
.
The house owner’s and the painter’s participants are connected to both domains, as illustrated in the following diagram.
The configuration file composability.conf
configures the two domains iou
and paint
and three participants.
canton {
features {
enable-preview-commands = yes
enable-testing-commands = yes
}
monitoring {
tracing.propagation = enabled
logging.api.message-payloads = true
}
domains {
iou {
public-api.port = 13018
admin-api.port = 13019
storage.type = memory
init.domain-parameters.unique-contract-keys = false
}
paint {
public-api.port = 13028
admin-api.port = 13029
storage.type = memory
init.domain-parameters.unique-contract-keys = false
}
}
participants {
participant1 {
ledger-api.port = 13011
admin-api.port = 13012
storage.type = memory
init.parameters.unique-contract-keys = false
}
participant2 {
ledger-api.port = 13021
admin-api.port = 13022
storage.type = memory
init.parameters.unique-contract-keys = false
}
participant3 {
ledger-api.port = 13031
admin-api.port = 13032
storage.type = memory
init.parameters.unique-contract-keys = false
}
}
}
As the first step, some domain parameters are changed (setting transfer-exclusivity-timeout
will be explained in
the second part of this tutorial). Then, all the nodes are started and the
parties for the bank (hosted on participant 1), the house owner (hosted on participant 2), and the painter (hosted on
participant 3) are created. The details of the party onboarding are not relevant for show-casing cross-domain workflows.
// update parameters
iou.service.update_dynamic_domain_parameters(
_.update(transferExclusivityTimeout = Duration.Zero)
) // disable automatic transfer-in
paint.service.update_dynamic_domain_parameters(
_.update(transferExclusivityTimeout = 2.seconds)
)
// connect participants to the domain
participant1.domains.connect_local(iou)
participant2.domains.connect_local(iou)
participant3.domains.connect_local(iou)
participant2.domains.connect_local(paint)
participant3.domains.connect_local(paint)
// the connect call will use the configured domain name as an alias. the configured
// name is the one used in the configuration file.
// in reality, all participants pick the alias names they want, which means that
// aliases are not unique, whereas a `DomainId` is. However, the
// alias is convenient, while the DomainId is a rather long string including a hash.
// therefore, for commands, we prefer to use a short alias instead.
val paintAlias = paint.name
val iouAlias = iou.name
// create the parties
val Bank = participant1.parties.enable("Bank")
val HouseOwner = participant2.parties.enable("House Owner")
val Painter = participant3.parties.enable("Painter")
// Wait until the party enabling has taken effect and has been observed at the participants
val partyAssignment = Set(Bank -> participant1, HouseOwner -> participant2, Painter -> participant3)
participant2.parties.await_topology_observed(partyAssignment)
participant3.parties.await_topology_observed(partyAssignment)
// upload the Daml model to all participants
val darPath = Option(System.getProperty("canton-examples.dar-path")).getOrElse("dars/CantonExamples.dar")
participants.all.dars.upload(darPath)
Creating the IOU and the paint offer¶
To initialize the ledger, the Bank creates an IOU for the house owner and the house owner creates a paint offer for the painter.
These steps are implemented below using the Scala bindings (no longer supported by Daml) generated from the Daml model.
The generated Scala classes are distributed with the Canton release in the package com.digitalasset.canton.examples
.
The relevant classes are imported as follows:
import com.digitalasset.canton.BigDecimalImplicits._
import com.digitalasset.canton.examples.java.iou.{Amount, Iou}
import com.digitalasset.canton.examples.java.paint.{OfferToPaintHouseByOwner, PaintHouse}
import com.digitalasset.canton.participant.ledger.api.client.JavaDecodeUtil.decodeAllCreated
import com.digitalasset.canton.protocol.ContractIdSyntax._
import scala.jdk.CollectionConverters._
Bank creates an IOU of USD 100 for the house owner on the iou
domain, by submitting the command through the ledger API command service of participant 1.
The house owner then shares the IOU contract with the painter such that the painter can effect the ownership change when they accept the offer.
The share operation adds the painter as an observer on the IOU contract so that the painter can see the IOU contract.
Both of these commands run over the iou
domain because the Bank’s participant 1 is only connected to the iou
domain.
// Bank creates IOU for the house owner
val createIouCmd = new Iou(
Bank.toProtoPrimitive,
HouseOwner.toProtoPrimitive,
new Amount(100.0.toBigDecimal, "USD"),
List.empty.asJava
).create.commands.asScala.toSeq
val Seq(iouContractUnshared) = decodeAllCreated(Iou.COMPANION)(
participant1.ledger_api.javaapi.commands.submit_flat(Seq(Bank), createIouCmd))
// Wait until the house owner sees the IOU in the active contract store
participant2.ledger_api.acs.await_active_contract(HouseOwner, iouContractUnshared.id.toLf)
// The house owner adds the Painter as an observer on the IOU
val shareIouCmd = iouContractUnshared.id.exerciseShare(Painter.toProtoPrimitive).commands.asScala.toSeq
val Seq(iouContract) = decodeAllCreated(Iou.COMPANION)(participant2.ledger_api.javaapi.commands.submit_flat(Seq(HouseOwner), shareIouCmd))
Similarly, the house owner creates a paint offer on the paint
domain via participant 2.
In the ledger_api.commands.submit_flat
command, we set the workflow id to the paint
domain so that the participant submits the commands to this domain.
If no domain was specified, the participant automatically determines a suitable domain.
In this case, both domains are eligible because on each domain, every stakeholder (the house owner and the painter) is hosted on a connected participant.
// The house owner creates a paint offer using participant 2 and the Paint domain
val paintOfferCmd = new OfferToPaintHouseByOwner(
HouseOwner.toProtoPrimitive,
Painter.toProtoPrimitive,
Bank.toProtoPrimitive,
iouContract.id
).create.commands.asScala.toSeq
val Seq(paintOffer) = decodeAllCreated(OfferToPaintHouseByOwner.COMPANION)(
participant2.ledger_api.javaapi.commands.submit_flat(Seq(HouseOwner), paintOfferCmd, workflowId = paint.name))
Contracts and Their Domains¶
In Canton, each contract is only known to the participants involved in that contract. The involved participants are the only ones that have unencrypteded copies of the contract, which they store in their respective private contract stores. No other participant has access to that data, even in encrypted form. The domain, in particular the sequencer that facilitates synchronization, will only store encrypted messages that only the receiving participant can decrypt.
In our terminology, the residence domain of a contract is the current agreement between the stakeholders of the contract where changes to the contract are to be communicated and where the sequence of actions on a contract is to be determined. A contract can reside on at most one domain at any point in time. However, the contract is never stored by the domain in such a way that the domain learns about its existence or content.
Transferring a contract¶
For example, the IOU contract resides on the iou
domain because it has been created by a command that was submitted to the iou
domain.
Similarly, the paint offer resides on the paint
domain.
In the current version of Canton, the execution of a transaction can only use contracts that reside on a single domain.
Therefore, before the painter can accept the offer and thereby become the owner of the IOU contract,
both contracts must be brought to a common domain.
In this example, the house owner and the painter are hosted on participants that are connected to both domains,
whereas the Bank is only connected to the iou
domain.
The IOU contract cannot be moved to the paint
domain because all stakeholders of a contract must be connected to the contract’s domain of residence.
Conversely, the paint offer can be transferred to the iou
domain, so that the painter can accept the offer on the iou
domain.
Stakeholders can change the residence domain of a contract using the transfer.execute
command.
In the example, the painter transfers the paint offer from the paint
domain to the iou
domain.
// Wait until the painter sees the paint offer in the active contract store
participant3.ledger_api.acs.await_active_contract(Painter, paintOffer.id.toLf)
// Painter transfers the paint offer to the IOU domain
participant3.transfer.execute(
Painter, // Initiator of the transfer
paintOffer.id.toLf, // Contract to be transferred
paintAlias, // Source domain
iouAlias // Target domain
)
The transfer of a contract effectively changes the residence domain of the contract, in other words, the consensus among the stakeholders on which domain should be used to sequence actions on a contract. The contract itself is still stored only on the involved participants.
Atomic acceptance¶
The paint offer and the IOU contract both reside on the iou
domain now.
Accordingly, the painter can complete the workflow by accepting the offer.
// Painter accepts the paint offer on the IOU domain
val acceptCmd = paintOffer.id.exerciseAcceptByPainter().commands.asScala.toSeq
val acceptTx = participant3.ledger_api.javaapi.commands.submit_flat(Seq(Painter), acceptCmd)
val Seq(painterIou) = decodeAllCreated(Iou.COMPANION)(acceptTx)
val Seq(paintHouse) = decodeAllCreated(PaintHouse.COMPANION)(acceptTx)
This transaction executes on the iou
domain because the input contracts (the paint offer and the IOU) reside there.
It atomically creates two contracts on the iou
domain: the painter’s new IOU and the agreement to paint the house.
The unhappy scenarios needing out-of-band resolution are avoided.
Completing the workflow¶
Finally, the paint agreement can be transferred back to the paint
domain, where it actually belongs.
// Wait until the house owner sees the PaintHouse agreement
participant2.ledger_api.acs.await_active_contract(HouseOwner, paintHouse.id.toLf)
// The house owner moves the PaintHouse agreement back to the Paint domain
participant2.transfer.execute(
HouseOwner,
paintHouse.id.toLf,
iouAlias,
paintAlias
)
Note that the painter’s IOU remains on the iou
domain.
The painter can therefore call the IOU and cash it out.
// Painter converts the Iou into cash
participant3.ledger_api.javaapi.commands.submit_flat(
Seq(Painter),
painterIou.id.exerciseCall().commands.asScala.toSeq,
iou.name
)
Performing transfers automatically¶
Canton also supports automatic transfers for commands performing transactions that use contracts residing on several domains. When such a command is submitted, Canton can automatically infer a common domain that the used contracts can be transferred to. Once all the used contracts have been transferred into the common domain the transaction is performed on this single domain. However, this simply performs the required transfers followed by the transaction processing as distinct non-atomic steps.
We can therefore run the above script without specifying any transfers at all, and relying on the automatic transfers. Simply delete all the transfer commands from the example above and the example will still run successfully. A modified version of the above example that uses automatic transfers instead of manual transfers is given below.
The setup code and contract creation is unchanged:
// Bank creates IOU for the house owner
val createIouCmd = new Iou(
Bank.toProtoPrimitive,
HouseOwner.toProtoPrimitive,
new Amount(100.0.toBigDecimal, "USD"),
List.empty.asJava
).create.commands.asScala.toSeq
val Seq(iouContractUnshared) = decodeAllCreated(Iou.COMPANION)(
participant1.ledger_api.javaapi.commands.submit_flat(Seq(Bank), createIouCmd))
// Wait until the house owner sees the IOU in the active contract store
participant2.ledger_api.acs.await_active_contract(HouseOwner, iouContractUnshared.id.toLf)
// The house owner adds the Painter as an observer on the IOU
val showIouCmd = iouContractUnshared.id.exerciseShare(Painter.toProtoPrimitive).commands.asScala.toSeq
val Seq(iouContract) = decodeAllCreated(Iou.COMPANION)(participant2.ledger_api.javaapi.commands.submit_flat(Seq(HouseOwner), showIouCmd))
// The house owner creates a paint offer using participant 2 and the Paint domain
val paintOfferCmd = new OfferToPaintHouseByOwner(
HouseOwner.toProtoPrimitive,
Painter.toProtoPrimitive,
Bank.toProtoPrimitive,
iouContract.id
).create.commands.asScala.toSeq
val Seq(paintOffer) = decodeAllCreated(OfferToPaintHouseByOwner.COMPANION)(
participant2.ledger_api.javaapi.commands.submit_flat(Seq(HouseOwner), paintOfferCmd, workflowId = paint.name))
In the following section, the painter accepts the paint offer. The transaction that accepts the paint offer uses two contracts: the paint offer contract, and the IOU contract. These contracts were created on two different domains in the previous step: the paint offer contract was created on the paint domain, and the IOU contract was created on the IOU domain. The paint offer contract must be transferred to the IOU domain for the accepting transaction to be successfully applied, as was done manually in the example above. It would not be possible to instead transfer the IOU contract to the paint domain because the stakeholder Bank on the IOU contract is not represented on the paint domain.
When using automatic-transfer transactions, Canton infers a suitable domain for the transaction and transfers all used contracts to this domain before applying the transaction. In this case, the only suitable domain for the painter to accept the paint offer is the IOU domain. This is how the painter is able to accept the paint offer below without any explicit transfers being performed.
// Wait until the painter sees the paint offer in the active contract store
participant3.ledger_api.acs.await_active_contract(Painter, paintOffer.id.toLf)
// Painter accepts the paint offer on the IOU domain
val acceptCmd = paintOffer.id.exerciseAcceptByPainter().commands.asScala.toSeq
val acceptTx = participant3.ledger_api.javaapi.commands.submit_flat(Seq(Painter), acceptCmd)
val Seq(painterIou) = decodeAllCreated(Iou.COMPANION)(acceptTx)
val Seq(paintHouse) = decodeAllCreated(PaintHouse.COMPANION)(acceptTx)
The painter can then cash in the IOU. This happens exactly as before, since the IOU contract never leaves the IOU domain.
// Painter converts the Iou into cash
participant3.ledger_api.javaapi.commands.submit_flat(
Seq(Painter),
painterIou.id.exerciseCall().commands.asScala.toSeq,
iou.name
)
Note that towards the end of the previous example with explicit transfers, the paint offer contract was transferred back to the paint domain. This doesn’t happen in the automatic transfer version: the paint offer is not transferred out of the IOU domain as part of the script shown. However, the paint offer contract will be automatically transferred back to the paint domain once it is used in a transaction that must happen on the paint domain.
Details of the automatic-transfer transactions¶
In the previous section, the automatic-transfer transactions were explained using an example. The details are presented here.
The automatic-transfer transactions enable submission of a transaction using contracts on multiple domains, by
transferring contracts into a chosen target domain and then performing the transaction. However, using an
automatic-transfer transaction does not provide any atomicity guarantees beyond using several primitive transfer-in
and transfer-out operations (these operations make up the transfer.execute
command, and are explained in the next section).
The domain for a transaction is chosen using the following criteria:
- Minimise the number of transfers needed.
- Break ties by choosing domains with higher priority first.
- Break ties by choosing domains with alphabetically smaller domain IDs first.
As for ordinary transactions, you may force the choice of domain for an automatic-transfer transaction by setting the workflow ID to name of the domain.
The automatic-transfer transactions are only enabled when all of the following are true:
- The local canton console enables preview commands (see the configuration section).
- The submitting participant is connected to all domains that contracts used by the transaction live on.
- All contracts used by the transaction must have at least one stakeholder that is also a transaction submitter.
Take aways¶
- A contract resides on a domain. This means that the current agreement of the stakeholders is to communicate and sequence all access and changes to a given contract on a particular domain. The contract itself is only stored at the stakeholder participants.
- Stakeholders can move contracts from one domain to another using
transfer.execute
. All stakeholders must be connected to the source and the target domain. - You can submit transactions using contracts that reside on several domains. Automatic transfers will pick a suitable domain, and perform the transfers into it before performing the transaction.
Part 2: Composing existing workflows¶
This part shows how existing workflows can be composed even if they work on separate domains. The running example is a variation of the paint example from the first part with a more complicated topology. We therefore assume that you have gone through the first part of this tutorial. Technically, this tutorial runs through the same steps as the first part, but more details are exposed. The console commands assume that you start with a fresh Canton console.
Existing workflows¶
Consider a situation where the two domains iou
and paint
have evolved separately:
- The
iou
domain for managing IOUs, - The
paint
domain for managing paint agreements.
Accordingly, there are separate applications for managing IOUs (issuing, changing ownership, calling) and paint agreements, and the house owner and the painter have connected their applications to different participants. The situation is illustrated in the following picture.
To enter in a paint agreement in this setting, the house owner and the painter need to perform the following steps:
- The house owner creates a paint offer through participant 2 on the
paint
domain. - The painter accepts the paint offer through participant 3 on the
paint
domain. As a consequence, a paint agreement is created. - The painter sets a reminder that he needs to receive an IOU from the house owner on the
iou
domain. - When the house owner observes a new paint agreement through participant 2 on the
paint
domain, she changes the IOU ownership to the painter through participant 5 on theiou
domain. - The painter observes a new IOU through participant 4 on the
iou
domain and therefore removes the reminder.
Overall, a non-trivial amount of out-of-band coordination is required
to keep the paint
ledger consistent with the iou
ledger.
If this coordination breaks down, the unhappy scenarios from the first part can happen.
Required changes¶
We now show how the house owner and the painter can avoid need for out-of-band coordination when entering in paint agreements. The goal is to reuse the existing infrastructure for managing IOUs and paint agreements as much as possible. The following changes are needed:
The house owner and the painter connect their participants for paint agreements to the
iou
domain:The Canton configuration is accordingly extended with the two participants 4 and 5. (The connections themselves are set up in the next section.)
canton { participants { participant4 { ledger-api.port = 13041 admin-api.port = 13042 storage.type = memory init.parameters.unique-contract-keys = false } participant5 { ledger-api.port = 13051 admin-api.port = 13052 storage.type = memory init.parameters.unique-contract-keys = false } } }
They replace their Daml model for paint offers such that the house owner must specify an IOU in the offer and its accept choice makes the painter the new owner of the IOU.
They create a new application for the paint offer-accept workflow.
The Daml models for IOUs and paint agreements themselves remain unchanged, and so do the applications that deal with them.
Preparation using the existing workflows¶
We extend the topology from the first part as described. The commands are explained in detail in Canton’s identity management manual.
// update parameters
iou.service.update_dynamic_domain_parameters(
_.update(transferExclusivityTimeout = Duration.Zero)
) // disable automatic transfer-in
paint.service.update_dynamic_domain_parameters(
_.update(transferExclusivityTimeout = 2.seconds)
)
// connect participants to the domain
participant1.domains.connect_local(iou)
participant2.domains.connect_local(iou)
participant3.domains.connect_local(iou)
participant2.domains.connect_local(paint)
participant3.domains.connect_local(paint)
participant4.domains.connect_local(iou)
participant5.domains.connect_local(iou)
val iouAlias = iou.name
val paintAlias = paint.name
// create the parties
val Bank = participant1.parties.enable("Bank")
val HouseOwner = participant2.parties.enable("House Owner")
val Painter = participant3.parties.enable("Painter", waitForDomain = DomainChoice.All)
// enable the house owner on participant 5 and the painter on participant 4
// as explained in the identity management documentation at
// https://docs.daml.com/canton/usermanual/identity_management.html#party-on-two-nodes
import com.digitalasset.canton.console.ParticipantReference
def authorizePartyParticipant(partyId: PartyId, createdAt: ParticipantReference, to: ParticipantReference): Unit = {
val createdAtP = createdAt.id
val toP = to.id
createdAt.topology.party_to_participant_mappings.authorize(TopologyChangeOp.Add, partyId, toP, RequestSide.From)
to.topology.party_to_participant_mappings.authorize(TopologyChangeOp.Add, partyId, toP, RequestSide.To)
}
authorizePartyParticipant(HouseOwner, participant2, participant5)
authorizePartyParticipant(Painter, participant3, participant4)
// Wait until the party enabling has taken effect and has been observed at the participants
val partyAssignment = Set(HouseOwner -> participant2, HouseOwner -> participant5, Painter -> participant3, Painter -> participant4)
participant2.parties.await_topology_observed(partyAssignment)
participant3.parties.await_topology_observed(partyAssignment)
// upload the Daml model to all participants
val darPath = Option(System.getProperty("canton-examples.dar-path")).getOrElse("dars/CantonExamples.dar")
participants.all.dars.upload(darPath)
As before, the Bank creates an IOU and the house owner shares it with the painter on the iou
domain, using their existing applications for IOUs.
import com.digitalasset.canton.BigDecimalImplicits._
import com.digitalasset.canton.examples.java.iou.{Amount, Iou}
import com.digitalasset.canton.examples.java.paint.{OfferToPaintHouseByOwner, PaintHouse}
import com.digitalasset.canton.participant.ledger.api.client.JavaDecodeUtil.decodeAllCreated
import com.digitalasset.canton.protocol.ContractIdSyntax._
import scala.jdk.CollectionConverters._
val createIouCmd = new Iou(
Bank.toProtoPrimitive,
HouseOwner.toProtoPrimitive,
new Amount(100.0.toBigDecimal, "USD"),
List.empty.asJava
).create.commands.asScala.toSeq
val Seq(iouContractUnshared) = decodeAllCreated(Iou.COMPANION)(
participant1.ledger_api.javaapi.commands.submit_flat(Seq(Bank), createIouCmd))
// Wait until the house owner sees the IOU in the active contract store
participant2.ledger_api.acs.await_active_contract(HouseOwner, iouContractUnshared.id.toLf)
// The house owner adds the Painter as an observer on the IOU
val shareIouCmd = iouContractUnshared.id.exerciseShare(Painter.toProtoPrimitive).commands.asScala.toSeq
val Seq(iouContract) = decodeAllCreated(Iou.COMPANION)(participant2.ledger_api.javaapi.commands.submit_flat(Seq(HouseOwner), shareIouCmd))
The paint offer-accept workflow¶
The new paint offer-accept workflow happens in four steps:
- Create the offer on the
paint
domain. - Transfer the contract to the
iou
domain. - Accept the offer.
- Transfer the paint agreement to the
paint
domain.
Making the offer¶
The house owner creates a paint offer on the paint
domain.
// The house owner creates a paint offer using participant 2 and the Paint domain
val paintOfferCmd = new OfferToPaintHouseByOwner(
HouseOwner.toProtoPrimitive,
Painter.toProtoPrimitive,
Bank.toProtoPrimitive,
iouContract.id
).create.commands.asScala.toSeq
val Seq(paintOffer) = decodeAllCreated(OfferToPaintHouseByOwner.COMPANION)(
participant2.ledger_api.javaapi.commands.submit_flat(Seq(HouseOwner), paintOfferCmd, workflowId = paint.name))
Transfers are not atomic¶
In the first part, we have used transfer.execute
to move the offer to the iou
domain.
Now, we look a bit behind the scenes.
A contract transfer happens in two atomic steps: transfer-out and transfer-in.
transfer.execute
is merely a shorthand for the two steps.
In particular, transfer.execute
is not an atomic operation like other ledger commands.
During a transfer-out, the contract is deactivated on the source domain, in this case the paint
domain.
Any stakeholder whose participant is connected to the source domain and the target domain can initiate a transfer-out.
The transfer.out
command returns a transfer Id.
// Wait until the painter sees the paint offer in the active contract store
participant3.ledger_api.acs.await_active_contract(Painter, paintOffer.id.toLf)
// Painter transfers the paint offer to the IOU domain
val paintOfferTransferId = participant3.transfer.out(
Painter, // Initiator of the transfer
paintOffer.id.toLf, // Contract to be transferred
paintAlias, // Source domain
iouAlias // Target domain
)
The transfer.in
command consumes the transfer Id and activates the contract on the target domain.
participant3.transfer.in(Painter, paintOfferTransferId, iouAlias)
Between the transfer-out and the transfer-in, the contract does not reside on any domain and cannot be used by commands. We say that the contract is in transit.
Accepting the paint offer¶
The painter accepts the offer, as before.
// Wait until the Painter sees the IOU contract on participant 3.
participant3.ledger_api.acs.await_active_contract(Painter, iouContract.id.toLf)
// Painter accepts the paint offer on the Iou domain
val acceptCmd = paintOffer.id.exerciseAcceptByPainter().commands.asScala.toSeq
val acceptTx = participant3.ledger_api.javaapi.commands.submit_flat(Seq(Painter), acceptCmd)
val Seq(painterIou) = decodeAllCreated(Iou.COMPANION)(acceptTx)
val Seq(paintHouse) = decodeAllCreated(PaintHouse.COMPANION)(acceptTx)
Automatic transfer-in¶
Finally, the paint agreement is transferred back to the paint
domain such that the existing infrastructure around paint agreements can work unchanged.
// Wait until the house owner sees the PaintHouse agreement
participant2.ledger_api.acs.await_active_contract(HouseOwner, paintHouse.id.toLf)
val paintHouseId = paintHouse.id
// The house owner moves the PaintHouse agreement back to the Paint domain
participant2.transfer.out(
HouseOwner,
paintHouseId.toLf,
iouAlias,
paintAlias
)
// After the exclusivity period, which is set to 2 seconds,
// the contract is automatically transferred into the target domain
utils.retry_until_true(10.seconds) {
// in the absence of other activity, force the participants to update their view of the latest domain time
participant2.testing.fetch_domain_times()
participant3.testing.fetch_domain_times()
participant3.testing.acs_search(paint.name, filterId=paintHouseId.contractId).nonEmpty &&
participant2.testing.acs_search(paint.name, filterId=paintHouseId.contractId).nonEmpty
}
Here, there is only a transfer.out
command but no transfer.in
command.
This is because the participants of contract stakeholders automatically try to transfer-in the contract to the target domain so that the contract becomes usable again.
The domain parameter transfer-exclusivity-timeout
on the target domain specifies how long they wait before they attempt to do so.
Before the timeout, only the initiator of the transfer is allowed to transfer-in the contract.
This reduces contention for contracts with many stakeholders, as the initiator normally completes the transfer before all other stakeholders simultaneously attempt to transfer-in the contract.
On the paint
domain, this timeout is set to two seconds in the configuration file.
Therefore, the utils.retry_until_true
normally succeeds within the allotted ten seconds.
Setting the transfer-exclusivity-timeout
to 0 as on the iou
domain disables automatic transfer-in.
This is why the above transfer of the paint offer had to be completed manually.
Manual completion is also needed if the automatic transfer-in fails, e.g., due to timeouts on the target domain.
Automatic transfer-in therefore is a safety net that reduces the risk that the contract gets stuck in transit.
Continuing the existing workflows¶
The painter now owns an IOU on the iou
domain and the entered paint agreement resides on the paint
domain.
Accordingly, the existing workflows for IOUs and paint agreements can be used unchanged.
For example, the painter can call the IOU.
// Painter converts the Iou into cash
participant4.ledger_api.javaapi.commands.submit_flat(
Seq(Painter),
painterIou.id.exerciseCall().commands.asScala.toSeq,
iou.name
)
Take aways¶
- Contract transfers take two atomic steps: transfer-out and transfer-in. While the contract is being transferred, the contract does not reside on any domain.
- Transfer-in happens under normal circumstances automatically after the
transfer-exclusivity-timeout
configured on the target domain. A timeout of 0 disables automatic transfer-in. If the automatic transfer-in does not complete, the contract can be transferred in manually.