Reference States Solution
- Reading time: 18 mins
- Discuss on Slack
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 commandsOffer
,Reject
andAccept
, 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:
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
tobuyer
at the givenprice
in the statedIssuedTokenType
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:
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:
Extract elements from the transaction:
final List<StateAndRef<AbstractToken>> inRefs = tx.referenceInputRefsOfType(AbstractToken.class);
final List<StateAndRef<SalesProposal>> inSalesProposals = tx.inRefsOfType(SalesProposal.class);
final List<StateAndRef<SalesProposal>> outSalesProposals = tx.outRefsOfType(SalesProposal.class);
As in so many contracts, the SalesProposal
is in output, not in input:
req.using("There should be no sales proposal inputs on offer",
inSalesProposals.isEmpty());
req.using("There should be a single sales proposal output on offer",
outSalesProposals.size() == 1);
-
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.
The price cannot be zero:
req.using("There should be no sales proposal inputs on offer",
inSalesProposals.isEmpty());
req.using("There should be a single sales proposal output on offer",
outSalesProposals.size() == 1);
The seller is the only signer:
req.using("The seller should be the only signer on the offer",
Collections.singletonList(proposal.getSeller().getOwningKey()).equals(command.getSigners()));
Note that you already have assurance that the seller is the holder of the asset
as per the SalesProposal
constructor.
On Reject
The proposal is consumed, and the asset remains untouched:
Extract elements from the transaction:
final List<StateAndRef<SalesProposal>> inSalesProposals = tx.inRefsOfType(SalesProposal.class);
final List<StateAndRef<SalesProposal>> outSalesProposals = tx.outRefsOfType(SalesProposal.class);
Unsurprisingly the SalesProposal
is in input, not in output:
req.using("There should be a single input sales proposal on reject",
inSalesProposals.size() == 1);
req.using("There should be no sales proposal outputs on reject",
outSalesProposals.isEmpty());
And any of the participants can reject the offer:
final SalesProposal proposal = inSalesProposals.get(0).getState().getData();
req.using("The seller or the buyer or both should be signers",
command.getSigners().contains(proposal.getSeller().getOwningKey()) ||
command.getSigners().contains(proposal.getBuyer().getOwningKey()));
req.using("Only the seller or the buyer or both should be signers",
Arrays.asList(proposal.getSeller().getOwningKey(), proposal.getBuyer().getOwningKey())
.containsAll(command.getSigners()));
On Accept
The proposal is consumed, and the asset changes hands for the agreed price.
Extract elements from the transaction:
final List<StateAndRef<SalesProposal>> inSalesProposals = tx.inRefsOfType(SalesProposal.class);
final List<StateAndRef<SalesProposal>> outSalesProposals = tx.outRefsOfType(SalesProposal.class);
final List<StateAndRef<NonFungibleToken>> inNFTokens = tx.inRefsOfType(NonFungibleToken.class);
final List<StateAndRef<NonFungibleToken>> outNFTokens = tx.outRefsOfType(NonFungibleToken.class);
final List<StateAndRef<FungibleToken>> outFTokens = tx.outRefsOfType(FungibleToken.class);
Unsurprisingly, the SalesProposal
is in input, not in output:
req.using("There should be a single input sales proposal on accept",
inSalesProposals.size() == 1);
req.using("There should be no sales proposal outputs on accept",
outSalesProposals.isEmpty());
final SalesProposal proposal = inSalesProposals.get(0).getState().getData();
The buyer has to sign off on it:
req.using("The buyer should be the only signer on the offer",
Collections.singletonList(proposal.getBuyer().getOwningKey()).equals(command.getSigners()));
-
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
FungibleToken
s. This contract does not verify that the buyer paid. It also leaves the possibility open to atomically mix it with other states and contracts.
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.
- And its handler
OfferSimpleFlow
, an initiating flow that takes easy information before passing on toOfferFlow
.- And its automatic handler
OfferSimpleHandlerFlow
.
- And its automatic handler
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
.
It expects accurate information as mentioned earlier. Note the presence of StateAndRef
, not StaticPointer
:
public OfferFlow(@NotNull final StateAndRef<NonFungibleToken> asset,
@NotNull final AbstractParty buyer,
@NotNull final Amount<IssuedTokenType> price,
@NotNull final ProgressTracker progressTracker) {
It creates the proposal:
final SalesProposal proposal = new SalesProposal(new UniqueIdentifier(), asset, buyer, price);
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 aReferencedStateAndRef
.
It verifies:
builder.verify(getServiceHub());
Signs locally:
final SignedTransaction offerTx = getServiceHub().signInitialTransaction(
builder, proposal.getSeller().getOwningKey());
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.
According to the choreography:
subFlow(new SyncKeyMappingFlowHandler(sellerSession));
return subFlow(new ReceiveFinalityFlow(sellerSession));
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:
- The car is issued to its owner.
- The DMV changes the mileage on the car but fails to inform anyone.
- 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.- And its handler
RejectHandlerFlow
, an inlined flow.
- And its handler
RejectSimpleFlow
, an initiating flow that takes easy information before passing on toRejectFlow
.- And its automatic handler
RejectSimpleHandlerFlow
.
- And its automatic handler
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);
Creates the transaction:
final TransactionBuilder builder = new TransactionBuilder(proposal.getState().getNotary())
.addInputState(proposal)
.addCommand(new SalesProposalContract.Commands.Reject(),
Collections.singletonList(rejecter.getOwningKey()));
Verifies and signs locally:
builder.verify(getServiceHub());
final SignedTransaction rejectTx = getServiceHub().signInitialTransaction(
builder, rejecter.getOwningKey());
It resolves the host of the rejectee:
final Party rejecteeHost = getServiceHub().getIdentityService()
.requireWellKnownPartyFromAnonymous(rejectee);
Before sending the result:
return subFlow(new FinalityFlow(rejectTx, initiateFlow(rejecteeHost)));
return subFlow(new ReceiveFinalityFlow(rejecterSession));
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) {
Namely fetching the proposal from the vault:
final QueryCriteria proposalCriteria = new QueryCriteria.LinearStateQueryCriteria()
.withUuid(Collections.singletonList(proposalId.getId()));
final List<StateAndRef<SalesProposal>> proposals = getServiceHub().getVaultService()
.queryBy(SalesProposal.class, proposalCriteria)
.getStates();
if (proposals.size() != 1) throw new FlowException("Wrong number of proposals found");
final StateAndRef<SalesProposal> proposal = proposals.get(0);
Then passes it on:
return subFlow(new RejectFlow(proposal, rejecter, PASSING_ON.childProgressTracker()));
final QueryCriteria proposalCriteria = new QueryCriteria.LinearStateQueryCriteria()
.withUuid(Collections.singletonList(proposalId.getId()));
final List<StateAndRef<SalesProposal>> proposals = getServiceHub().getVaultService()
.queryBy(SalesProposal.class, proposalCriteria)
.getStates();
if (proposals.size() != 1) throw new FlowException("Wrong number of proposals found");
final StateAndRef<SalesProposal> proposal = proposals.get(0);
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
, anabstract
inlined flow that expects perfect information and executes the mechanical parts of the flow to accept the sales proposal and affect the sale.- And its handler
AcceptHandlerFlow
, an inlined flow.
- And its handler
AcceptSimpleFlow
, an initiating flow that takes easy information before passing on toAcceptFlow
.- And its automatic handler
AcceptSimpleHandlerFlow
.
- And its automatic handler
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) {
First, by adding the proposal. Note how the pointer is resolve
d:
final SalesProposal proposal = proposalRef.getState().getData();
final NonFungibleToken asset = proposal.getAsset().resolve(getServiceHub()).getState().getData();
final TransactionBuilder builder = new TransactionBuilder(proposalRef.getState().getNotary())
.addInputState(proposalRef)
.addCommand(new SalesProposalContract.Commands.Accept(),
Collections.singletonList(proposal.getBuyer().getOwningKey()));
Then, by adding the asset for themselves:
MoveTokensUtilitiesKt.addMoveNonFungibleTokens(builder, getServiceHub(),
asset.getToken().getTokenType(), proposal.getBuyer());
Then, by collecting tokens for payment:
final IssuedTokenType issuedCurrency = proposal.getPrice().getToken();
final QueryCriteria heldByBuyer = getHeldByBuyer(issuedCurrency, proposal.getBuyer());
final Amount<TokenType> priceInCurrency = new Amount<>(
proposal.getPrice().getQuantity(),
proposal.getPrice().getToken());
// Generate the buyer's currency inputs, to be spent, and the outputs, the currency tokens that will be
// held by the seller.
final DatabaseTokenSelection tokenSelection = new DatabaseTokenSelection(
getServiceHub(), MAX_RETRIES_DEFAULT, RETRY_SLEEP_DEFAULT, RETRY_CAP_DEFAULT, PAGE_SIZE_DEFAULT);
final Pair<List<StateAndRef<FungibleToken>>, List<FungibleToken>> moniesInOut = tokenSelection.generateMove(
// Eventually held by the seller.
Collections.singletonList(new Pair<>(proposal.getSeller(), priceInCurrency)),
// We see here that we should not rely on the default value, because the buyer keeps the change.
proposal.getBuyer(),
new TokenQueryBy(
issuedCurrency.getIssuer(),
(Function1<? super StateAndRef<? extends FungibleToken>, Boolean> & Serializable) it -> true,
heldByBuyer),
getRunId().getUuid());
MoveTokensUtilitiesKt.addMoveTokens(builder, moniesInOut.getFirst(), moniesInOut.getSecond());
Somewhat weirdly, the it -> true
lambda needs to be cast as Serializable
for Quasar.
builder.verify(getServiceHub());
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);
Collecting signatures from the seller:
final SignedTransaction signedTx = subFlow(new CollectSignaturesFlow(
acceptTx,
Collections.singletonList(sellerSession),
ourKeys,
GATHERING_SIGS.childProgressTracker()));
And finalising it:
return subFlow(new FinalityFlow(
signedTx,
Collections.singletonList(sellerSession),
FINALISING_TRANSACTION.childProgressTracker()));
Speaking of which, on the AcceptHandlerFlow
:
// Potentially missing StateRefs
subFlow(new ReceiveStateAndRefFlow<>(buyerSession));
// Receive potentially missing keys.
subFlow(new SyncKeyMappingFlowHandler(buyerSession));
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");
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.
return subFlow(new ReceiveFinalityFlow(buyerSession, txId));
AcceptSimpleFlow
Similarly to RejectSimpleFlow
, it takes in minimal information:
public AcceptSimpleFlow(
@NotNull final UniqueIdentifier proposalId,
@NotNull final ProgressTracker progressTracker) {
final QueryCriteria proposalCriteria = new QueryCriteria.LinearStateQueryCriteria()
.withUuid(Collections.singletonList(proposalId.getId()));
final List<StateAndRef<SalesProposal>> proposals = getServiceHub().getVaultService()
.queryBy(SalesProposal.class, proposalCriteria)
.getStates();
if (proposals.size() != 1) throw new FlowException("Wrong number of proposals found");
final StateAndRef<SalesProposal> proposal = proposals.get(0);
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.