/
Launch Online IDE

Reference States Solution

An example of a sales proposal and acceptance


Reading Time: 20 min

So, you worked on the exercise on your own before landing here and looking at this example solution, right? This solution comprises the following parts:

  • A new SalesProposal state found here.
  • A new SalesProposalContract contract with 3 commands Offer, Reject and Accept, found here.
  • 3 new flow pairs found here:
    • SalesProposalOfferFlows
    • SalesProposalRejectFlows
    • SalesProposalAcceptFlows

To follow along with IntelliJ, you need to open the 050-ref-state folder as a project. Let's review these in order.

SalesProposal state

Here is its CDL:

Sales Proposal State
Sales Proposal State

It makes sense to declare it as a LinearState as it has a linear lifecycle. It declares the following important fields:

@NotNull
private final StaticPointer<NonFungibleToken> asset;
@NotNull
private final AbstractParty buyer;
@NotNull
private final Amount<IssuedTokenType> price;

In other words:

I am willing to sell this asset to buyer at the given price in the stated IssuedTokenType currency.

Who is willing to sell it? Well, the car token holder obviously. However just the pointer is not enough to access the underlying NonFungibleToken, so the data is declared, for information only, explicitly:

@NotNull
private final AbstractParty seller;

For similar reasons, it also expects the underlying asset id:

@NotNull
private final UniqueIdentifier assetId;

Notice that the asset is of type StaticPointer<NonFungibleToken>, not just NonFungibleToken, so as to facilitate cross-checks with the state referenced in the transaction. Neither is it of type LinearId which would be a weak cross-check, even with a reference state. Additionally, it is unfalsifiable.

Admittedly, an error could have been made when instantiating the SalesProposal, whereby the pointed asset may not match the assetId and seller. In this case, the resolved asset shall be authoritative. To mitigate this type of of mistakes, there is an additional constructor that takes in a fully-resolved StateAndRef<NonFungibleToken> asset before passing on to the flat constructor. To avoid confusion (which constructor?) at deserialisation, the flat constructor is annotated with @ConstructorForDeserialization.

Why not use a fully resolved StateAndRef<NonFungibleToken> asset? It would work, at the cost of double serialisation of the NonFungibleToken content, itself and as part of a SalesProposal. A static pointer is more elegant and parcimonious.

Unremarkably, the participants are the buyer and the seller:

@NotNull
@Override
public List<AbstractParty> getParticipants() {
    return ImmutableList.copyOf(Arrays.asList(seller, buyer));
}

Finally, there is an extra function to facilitate later identification of the asset:

public boolean isSameAsset(@NotNull final StateAndRef<? extends ContractState> asset) {
    final ContractState aToken = asset.getState().getData();
    if (!(aToken instanceof NonFungibleToken)) return false;
    final NonFungibleToken token = (NonFungibleToken) aToken;
    return this.asset.getPointer().equals(asset.getRef())
            && this.assetId.equals(token.getLinearId())
            && this.seller.equals(token.getHolder());
}

SalesProposalContract

Here is its CDL:

Sales Proposal State Machine View
Sales Proposal State Machine View

It defines 3 commands:

  • Offer, whereby the seller declares its intention to sell the verified asset.
  • Reject, whereby either party rejects the offer.
  • Accept, whereby the buyer declares its acceptance of the offer and executes the purchase at the same time.

So, it makes sense for the contract to perform the following checks:

On Offer

A SalesProposal is created, and the asset is passed as a reference:

  • There is a single reference state:

    req.using("There should be a single reference input token on offer",
            inRefs.size() == 1);
  • Ooh! Something new. The referenced token should match the asset for sale:

    final StateAndRef<AbstractToken> refToken = inRefs.get(0);
    req.using("The reference token should match the sales proposal output asset",
            proposal.isSameAsset(refToken));

    It is here that the benefit of reference states is delivered. You are sure that:

    • The asset exists, for real.
    • The seller owns the token.
    • The asset is not sold yet. It is unconsumed.

On Reject

The proposal is consumed, and the asset remains untouched:

On Accept

The proposal is consumed, and the asset changes hands for the agreed price.

  • The asset, which was passed as a reference state on Offer, has to be a normal input this time:

    final List<StateAndRef<NonFungibleToken>> candidates = inNFTokens.stream()
            .filter(proposal::isSameAsset)
            .collect(Collectors.toList());
    req.using("The asset should be an input on accept", candidates.size() == 1);

    As an important side node, this means that if the seller had created 2 proposals for the same asset, then only 1 proposal can be accepted. Also, because the asset would have changed hands, so the seller would not be able to sign the asset off.

  • The buyer should own the asset on output:

    final List<NonFungibleToken> boughtAsset = outNFTokens.stream()
            .map(it -> it.getState().getData())
            .filter(it -> it.getLinearId().equals(proposal.getAssetId()))
            .collect(Collectors.toList());
    req.using("The asset should be held by buyer in output on accept",
            boughtAsset.size() == 1 && boughtAsset.get(0).getHolder().equals(proposal.getBuyer()));

    Note that there is only a need to check the holder, and no need to check that it is referring to the same underlying asset as this part is taken care of by the token contract itself.

  • The seller should be paid the agreed amount in return:

    final long sellerPayment = outFTokens.stream()
            .map(it -> it.getState().getData())
            .filter(it -> it.getHolder().equals(proposal.getSeller()))
            .filter(it -> it.getIssuedTokenType().equals(proposal.getPrice().getToken()))
            .map(it -> it.getAmount().getQuantity())
            .reduce(0L, Math::addExact);
    req.using("The seller should be paid the agreed amount in the agreed issued token on accept",
            proposal.getPrice().getQuantity() <= sellerPayment);
  • Note the absence of checks on the input FungibleTokens. This contract does not verify that the buyer paid. It also leaves the possibility open to atomically mix it with other states and contracts.

info icon

If your SalesProposal contract does not verify these parts:

  • That the asset changed hands on Accept.
  • That the seller was paid on Accept.

It is entirely fine as the purpose of the SalesProposal is to encapsulate an offer by the seller, and nothing more really. It is not a breakage of the ledger layer if the asset did not change hands or changed hands with the wrong payment. Plus, armed with a proposal whose creation is it itself signed, the seller's responder flow can run these mechanical checks.

This is to say that here there is some leeway as to what you decide to include in the contract. The decision was made here to off-load the responder flow of those checks, at the expense of future transaction flexibility.

Tests

Once more, there is 1 test file per command, and each failure point is tested. .tweak is used to make explicit which point is the failure point.

Time to move on to flows.

The SalesProposal offer flows

This is launched by the seller. The fact that the seller decides to initiate a run of this flow indicates intent to sell the asset. From there, the presence of a SalesProposal state is proof enough of the original intent.

There are actually 2 flow pairs in the SalesProposalOfferFlows set, as can be seen here:

  • OfferFlow, an inlined flow that expects perfect information and executes the mechanical parts of the flow to issue a sales proposal.
    • And its handler OfferHandlerFlow, an inlined flow.
  • OfferSimpleFlow, an initiating flow that takes easy information before passing on to OfferFlow.
    • And its automatic handler OfferSimpleHandlerFlow.

Offer Sales Proposal
Offer Sales Proposal

OfferFlow

This inlined flow handles the mechanical parts to issue an offer. It is started by the seller, and the potential buyer only has to finalize a complete transaction to receive the SalesProposal.

Then it starts creating the transaction:

final TransactionBuilder builder = new TransactionBuilder(asset.getState().getNotary())
        .addOutputState(proposal)
        .addReferenceState(new ReferencedStateAndRef<>(asset))
        .addCommand(new SalesProposalContract.Commands.Offer(),
                Collections.singletonList(proposal.getSeller().getOwningKey()));

Note how:

  • It picks the notary from the asset.
  • It adds the proposal and lets the system select the right contract.
  • It adds the asset as a ReferencedStateAndRef.

Then it needs to resolve the host of the buyer's public key before informing them:

final Party buyerHost = getServiceHub().getIdentityService()
        .requireWellKnownPartyFromAnonymous(proposal.getBuyer());
final FlowSession buyerSession = initiateFlow(buyerHost);

It preemptively informs the buyer's host about who the seller is:

subFlow(new SyncKeyMappingFlow(buyerSession, Collections.singletonList(proposal.getSeller())));

And finishes with finalisation:

return subFlow(new FinalityFlow(
        offerTx,
        Collections.singletonList(buyerSession),
        FINALISING_TRANSACTION.childProgressTracker()));

As part of the finalisation, the notary will confirm that the reference state of the asset, and if applicable the reference state of the asset type, are the correct, i.e. latest, ones.

As you can see, this is a very simple flow that uses a single reference and does not require a remote signature.

OfferSimpleFlow

This initiating flow is meant to make life simpler by requiring simplified information before passing on to OfferFlow:

public OfferSimpleFlow(@NotNull final UniqueIdentifier assetId,
                       @NotNull final Party buyer,
                       final long price,
                       @NotNull final String currencyCode,
                       @NotNull final Party issuer,
                       @NotNull final ProgressTracker progressTracker) {

You will recognize how these fields will be combined. First, the asset needs to be fetched from the vault:

final QueryCriteria assetCriteria = new QueryCriteria.LinearStateQueryCriteria()
        .withUuid(Collections.singletonList(assetId.getId()));
final List<StateAndRef<NonFungibleToken>> assets = getServiceHub().getVaultService()
        .queryBy(NonFungibleToken.class, assetCriteria)
        .getStates();
if (assets.size() != 1) throw new FlowException("Wrong number of assets found");
final StateAndRef<NonFungibleToken> asset = assets.get(0);

With this, it is just a matter of passing it on to OfferFlow:

return subFlow(new OfferFlow(asset, buyer,
        AmountUtilitiesKt.amount(price, new IssuedTokenType(issuer, currency)),
        PASSING_ON.childProgressTracker()));

With the handler simply being a child class of OfferFlowHandler:

@InitiatedBy(OfferSimpleFlow.class)
class OfferSimpleHandlerFlow extends OfferHandlerFlow {

Offer tests

In the tests there are 2 happy paths and 1 failed path. Take some time to look at the failed path. Here is what happens in it:

  1. The car is issued to its owner.
  2. The DMV changes the mileage on the car but fails to inform anyone.
  3. The car owner cannot create a sales proposal for it.

That's because the StateAndRef<CarTokenType has changed, and been recorded so by the notary. The host alice only has the old version in her vault. This is a side-effect of the fact that the state pointers are resolved when a state is added as a reference.

The second happy path confirms that the seller can create a sales proposal if it has been informed of the new car state.

The SalesProposal reject flows

This is launched by either the seller or the buyer. The one starting the flow is called the rejecter, the other, the rejectee.

In fact there are 2 flow pairs which can be found here:

  • RejectFlow, an inlined flow that expects perfect information and executes the mechanical parts of the flow to reject a sales proposal.
  • RejectSimpleFlow, an initiating flow that takes easy information before passing on to RejectFlow.

RejectFlow

This inlined flow handles the mechanical parts of a rejection. It expects exact information:

public RejectFlow(@NotNull final StateAndRef<SalesProposal> proposal,
                  // The rejecter is either the buyer or the seller.
                  @NotNull final AbstractParty rejecter,
                  @NotNull final ProgressTracker progressTracker) {

And deduces the rejectee:

this.rejectee = proposal.getState().getData().getParticipants().stream()
        .filter(it -> !it.equals(rejecter))
        .collect(Collectors.toList())
        .get(0);

Either party of the SalesProposal. Yes, it means that the seller can shut the door on the buyer up to the last minute. You will see in the next chapter how further assurances can be extended to the buyer against such tactics.

RejectSimpleFlow

Similarly to what you saw with OfferSimpleFlow, it takes simplified parameters:

public RejectSimpleFlow(
        @NotNull final UniqueIdentifier proposalId,
        @NotNull final AbstractParty rejecter,
        @NotNull final ProgressTracker progressTracker) {

Reject tests

They naturally check that either party can reject, which should inform the other.

The SalesProposal accept flows

This is launched by the buyer. The fact that the buyer decides to initiate a run of this flow indicates intent to buy the asset.

There are actually 2 flow pairs in the SalesProposalAccepFlows set, as can be seen here:

  • AcceptFlow, an abstract inlined flow that expects perfect information and executes the mechanical parts of the flow to accept the sales proposal and affect the sale.
  • AcceptSimpleFlow, an initiating flow that takes easy information before passing on to AcceptFlow.

Accept: Atomic Car Buy BPMN
Accept: Atomic Car Buy BPMN

AcceptFlow

It is abstract in the same way that AtomicSaleAccountsSafe.CarSellerFlow is, so as to give the ability to pick payment tokens:

abstract protected QueryCriteria getHeldByBuyer(
        @NotNull final IssuedTokenType issuedCurrency,
        @NotNull final AbstractParty buyer) throws FlowException;

Other than that, it looks very much like CarSellerFlow. It takes the expected information:

public AcceptFlow(@NotNull final StateAndRef<SalesProposal> proposalRef,
                  @NotNull final ProgressTracker progressTracker) {

Then, it moves on to send the token states to the seller session, so that the seller can verify:

final Party sellerHost = getServiceHub().getIdentityService()
        .requireWellKnownPartyFromAnonymous(proposal.getSeller());
final FlowSession sellerSession = initiateFlow(sellerHost);
// Send potentially missing StateRefs blindly.
subFlow(new SendStateAndRefFlow(sellerSession, moniesInOut.getFirst()));

With the potentially missing keys used in them:

final List<AbstractParty> moniesKeys = moniesInOut.getFirst().stream()
        .map(it -> it.getState().getData().getHolder())
        .collect(Collectors.toList());
subFlow(new SyncKeyMappingFlow(sellerSession, moniesKeys));

Then, it collects all the keys relevant to the buyer to sign locally:

final List<PublicKey> ourKeys = moniesKeys.stream()
        .map(AbstractParty::getOwningKey)
        .collect(Collectors.toList());
ourKeys.add(proposal.getBuyer().getOwningKey());
final SignedTransaction acceptTx = getServiceHub().signInitialTransaction(builder, ourKeys);
info icon

You will notice that the seller can still reject the sale, simply by refusing to sign this transaction.

Speaking of which, on the AcceptHandlerFlow:

Then, it prepares itself to sign the sale transaction:

final SecureHash txId = subFlow(new SignTransactionFlow(buyerSession) {

In which it verifies that there is a single Accept command:

final List<Command<?>> commands = stx.getTx().getCommands().stream()
        .filter(it -> it.getValue() instanceof SalesProposalContract.Commands.Accept)
        .collect(Collectors.toList());
if (commands.size() != 1)
    throw new FlowException("There is no accept command");
warn icon

The above covers a potentially sneaky fraud. Imagine that Alice, the seller, made 2 proposals, 1 for Bob at 10k and 1 for Carly at 11k. By hook or crook, Carly got hold of Bob's proposal from Alice and she wants to purchase the car at the cheaper price.

Carly could try to send a transaction with Bob's proposal, a Reject command with Alice as the required signer and 10k of tokens from Carly.

Because the Accept command requires a signature from the buyer, only Bob can sign an Accept on his proposal.

If the command verification above was missing, Alice's handler flow would be unaware of the undercover switch.

It then assembles the different types of input states expected:

final List<SalesProposal> proposals = new ArrayList<>(1);
final List<NonFungibleToken> assetsIn = new ArrayList<>(1);
final List<FungibleToken> moniesIn = new ArrayList<>(stx.getInputs().size());
for (final StateRef ref : stx.getInputs()) {
    final ContractState state = getServiceHub().toStateAndRef(ref).getState().getData();
    if (state instanceof SalesProposal)
        proposals.add((SalesProposal) state);
    else if (state instanceof NonFungibleToken)
        assetsIn.add((NonFungibleToken) state);
    else if (state instanceof FungibleToken)
        moniesIn.add((FungibleToken) state);
    else
        throw new FlowException("Unexpected state class: " + state.getClass());
}
if (proposals.size() != 1) throw new FlowException("There should be a single sales proposal in");
if (assetsIn.size() != 1) throw new FlowException("There should be a single asset in");
final SalesProposal proposal = proposals.get(0);
final NonFungibleToken assetIn = assetsIn.get(0);

Before confirming that no other key is required for signature:

final List<PublicKey> allInputKeys = moniesIn.stream()
        .map(it -> it.getHolder().getOwningKey())
        .collect(Collectors.toList());
allInputKeys.add(assetIn.getHolder().getOwningKey());
final List<PublicKey> myKeys = StreamSupport.stream(
        getServiceHub().getKeyManagementService().filterMyKeys(allInputKeys).spliterator(),
        false)
        .collect(Collectors.toList());
if (myKeys.size() != 1) throw new FlowException("There are not the expected keys of mine");
if (!myKeys.get(0).equals(proposal.getSeller().getOwningKey()))
    throw new FlowException("The key of mine is not the seller");

And, that the buyer is not trying to have the seller "pay themselves" for the privilege:

final List<FungibleToken> myInMonies = moniesIn.stream()
        .filter(it -> it.getHolder().equals(proposal.getSeller()))
        .collect(Collectors.toList());
if (!myInMonies.isEmpty())
    throw new FlowException("There is a FungibleToken of mine in input");

Note that the checks that the seller got paid is taken care of by the contract.

AcceptSimpleFlow

Similarly to RejectSimpleFlow, it takes in minimal information:

public AcceptSimpleFlow(
        @NotNull final UniqueIdentifier proposalId,
        @NotNull final ProgressTracker progressTracker) {

Then, passing it on to AcceptFlow. Not to forget to provide the criteria to pick tokens:

return subFlow(new AcceptFlow(proposal, PASSING_ON.childProgressTracker()) {
    @NotNull
    @Override
    protected QueryCriteria getHeldByBuyer(
            @NotNull final IssuedTokenType issuedCurrency,
            @NotNull final AbstractParty buyer) throws FlowException {
        return QueryUtilitiesKt.heldTokenAmountCriteria(issuedCurrency.getTokenType(), buyer);
    }
});

On the other end, AcceptSimpleHandlerFlow is just a child class of AcceptHandlerFlow:

class AcceptSimpleHandlerFlow extends AcceptHandlerFlow {

Accept tests

They test:

* That a [sale is possible](https://github.com/corda/corda-training-code/blob/master/050-ref-state/workflows/src/test/java/com/template/proposal/flow/SalesProposalAcceptFlowsTests.java#L159) when all conditions are present.
* That a sale is not possible if the token type has changed [without informing the buyer](https://github.com/corda/corda-training-code/blob/master/050-ref-state/workflows/src/test/java/com/template/proposal/flow/SalesProposalAcceptFlowsTests.java#L241).
* That a sale is not possible if the buyer [does not have enough dollars](https://github.com/corda/corda-training-code/blob/master/050-ref-state/workflows/src/test/java/com/template/proposal/flow/SalesProposalAcceptFlowsTests.java#L291).

Conclusion

As you have seen, the intent of selling and buying is split into 2 transactions. The SalesProposal, by its very existence, proves the intent of the seller with regards to asset, price and currency. Therefore, the AcceptHandlerFlow on the seller side can recognize its own intent, and is satisfied with classic mechanical verifications against fraud.

It was a design decision to keep a lot of verifications in the contract on accept, but a lighter version is also possible, where the fraud verifications are shifted to the flows. In this case, the SalesProposal serves only as the repository for the seller's intent.

As an additional note, now that you have a safe atomic sale mechanism, which expresses a desired price, the time has come to remove the price from CarTokenType.

In the next exercise, you will look at how you can protect the buyer from having the door shut too early by the seller. This assures the buyer that the offer is indeed open to acceptance.

Discuss on Slack
Rate this Page
Would you like to add a message?
Submit
Thank you for your Feedback!