Skip to content

Commit 9892bfd

Browse files
committed
code review changes from Aga
1 parent ace2473 commit 9892bfd

File tree

8 files changed

+110
-8
lines changed

8 files changed

+110
-8
lines changed

Features/encumbrance-avatar/README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,18 @@ Create the Avatar on PartyA node
5858

5959
Sell the Avatar to PartyB node from PartyA node
6060

61-
start SellAvatarFlow avatarId : 1, buyer : PartyB , includeEncumbrance : true
61+
start SellAvatarFlow avatarId : 1, buyer : PartyB
6262

6363
Confirm if PartyB owns the Avatar
6464

6565
run vaultQuery contractStateType : com.template.states.Avatar
6666

67+
Note
68+
As you can see in both the flows, Avatar is encumbered by Expiry. But Encumbrances should form a complete directed cycle,
69+
otherwise one can spend the "encumbrance" (Expiry) state, which would freeze the "encumbered" (Avatar) state for ever.
70+
That's why we also make Expiry dependent on Avatar. (See how we have added encumbrance index's to the output states in
71+
both the flows.)
72+
6773
## Reminder
6874

6975
This project is open source under an Apache 2.0 licence. That means you

Features/encumbrance-avatar/contracts/build.gradle

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,7 @@ dependencies {
1919
// Corda dependencies.
2020
cordaCompile "$corda_release_group:corda-core:$corda_release_version"
2121
cordaRuntime "$corda_release_group:corda:$corda_release_version"
22-
testCompile "$corda_release_group:corda-node-driver:$corda_release_version"
22+
cordaCompile "$corda_release_group:corda-node-driver:$corda_release_version"
23+
24+
testCompile "junit:junit:$junit_version"
2325
}

Features/encumbrance-avatar/contracts/src/main/java/com/template/contracts/ExpiryContract.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import java.time.LocalDateTime;
1111
import java.time.ZoneId;
1212

13+
//ExpiryContract is also run when Avatar's contract is run
1314
public class ExpiryContract implements Contract {
1415

1516
public static final String EXPIRY_CONTRACT_ID = "com.template.contracts.ExpiryContract";
@@ -26,6 +27,9 @@ public void verify(@NotNull LedgerTransaction tx) throws IllegalArgumentExceptio
2627
if (timeWindow == null || timeWindow.getUntilTime() == null) {
2728
throw new IllegalArgumentException("Make sure you specify the time window for the Avatar transaction.");
2829
}
30+
31+
//Expiry time should be after the time window, if the avatar expires before the time window, then the avatar
32+
//cannot be sold
2933
if (timeWindow.getUntilTime().isAfter(expiry.getExpiry())) {
3034
throw new IllegalArgumentException("Avatar transfer time has expired! Expiry date & time was: " + LocalDateTime.ofInstant(expiry.getExpiry(), ZoneId.systemDefault()));
3135
}

Features/encumbrance-avatar/contracts/src/main/java/com/template/states/Avatar.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import java.util.List;
1111
import java.util.Objects;
1212

13+
//Avatar can be thought of as any metaverse avatar which needs to be created and sold on at an exchange. This entity
14+
//has an id and owner associated with it. We will see how this avatar can only be sold within a certain time limit.
1315
@BelongsToContract(AvatarContract.class)
1416
public class Avatar implements ContractState {
1517
private final AbstractParty owner;

Features/encumbrance-avatar/contracts/src/main/java/com/template/states/Expiry.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import java.util.List;
1212
import java.util.Objects;
1313

14+
//Expiry represents an expiry date beyond which the avatar cannot be sold. This is the encumbrance state which encumbers
15+
//the Avatar state.
1416
@BelongsToContract(ExpiryContract.class)
1517
public class Expiry implements ContractState {
1618

Features/encumbrance-avatar/workflows/src/main/java/com/template/flows/CreateAvatarFlow.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,10 @@ public SignedTransaction call() throws FlowException {
3737
Avatar avatar = new Avatar(this.getOurIdentity(), avatarId);
3838
Expiry expiry = new Expiry(Instant.now().plus(expiryAfterMinutes, ChronoUnit.MINUTES), avatarId, avatar.getOwner());
3939

40+
//add expiry and avatar as outputs by specifying encumbrance as index. add time window
4041
TransactionBuilder txBuilder = new TransactionBuilder(notary)
41-
.addOutputState(avatar, AvatarContract.AVATAR_CONTRACT_ID, notary, 1)
42-
.addOutputState(expiry, ExpiryContract.EXPIRY_CONTRACT_ID, notary, 0)
42+
.addOutputState(avatar, AvatarContract.AVATAR_CONTRACT_ID, notary, 1) //specify the encumbrance as the 3rd parameter
43+
.addOutputState(expiry, ExpiryContract.EXPIRY_CONTRACT_ID, notary, 0) //specify the encumbrance as the 3rd parameter
4344
.addCommand(new AvatarContract.Commands.Create(), avatar.getOwner().getOwningKey())
4445
.addCommand(new ExpiryContract.Commands.Create(), expiry.getOwner().getOwningKey())
4546
.setTimeWindow(Instant.now(), Duration.ofSeconds(10));

Features/encumbrance-avatar/workflows/src/main/java/com/template/flows/SellAvatarFlow.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,21 +36,25 @@ public SignedTransaction call() throws FlowException {
3636

3737
Party buyerParty = getServiceHub().getIdentityService().partiesFromName(buyer, true).iterator().next();
3838

39-
Party notary = getServiceHub().getNetworkMapCache().getNotaryIdentities().get(0);
39+
Party notary = getServiceHub().getNetworkMapCache().getNotaryIdentities().get(0);
4040

41+
//get avatar from db
4142
Vault.Page<Avatar> avatarPage = getServiceHub().getVaultService().queryBy(Avatar.class);
4243
StateAndRef<Avatar> avatarStateAndRef = avatarPage.getStates().stream().filter(i ->
4344
i.getState().getData().getAvatarId().equalsIgnoreCase(avatarId)).findAny().orElseThrow(() ->
4445
new CordaRuntimeException("No avatar found with avatar id as : " + avatarId));;
4546

47+
//get expiry from db
4648
Vault.Page<Expiry> expiryPage = getServiceHub().getVaultService().queryBy(Expiry.class);
4749
StateAndRef<Expiry> expiryStateAndRef = expiryPage.getStates().stream().filter(i ->
4850
i.getState().getData().getAvatarId().equalsIgnoreCase(avatarId)).findAny().orElseThrow(() ->
4951
new CordaRuntimeException("No expiry found with avatar id as " + avatarId));
5052

53+
//change owner
5154
Avatar avatar = new Avatar(buyerParty, avatarId);
52-
Expiry expiry = new Expiry(expiryStateAndRef.getState().getData().getExpiry(), avatarId, avatar.getOwner());
55+
Expiry expiry = new Expiry(expiryStateAndRef.getState().getData().getExpiry(), avatarId, buyerParty);
5356

57+
//consume existing states, encumbering states will trigger the expiry contract to run
5458
TransactionBuilder transactionBuilder = new TransactionBuilder(notary)
5559
.addInputState(avatarStateAndRef)
5660
.addInputState(expiryStateAndRef)
Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,94 @@
11
package com.template;
22

3+
import com.google.common.collect.ImmutableList;
4+
import com.template.contracts.AvatarContract;
5+
import com.template.contracts.ExpiryContract;
6+
import com.template.states.Avatar;
7+
import com.template.states.Expiry;
8+
import net.corda.core.identity.CordaX500Name;
9+
import net.corda.testing.core.TestIdentity;
310
import net.corda.testing.node.MockServices;
411
import org.junit.Test;
512

13+
import java.time.Duration;
14+
import java.time.Instant;
15+
import java.time.temporal.ChronoUnit;
16+
17+
import static net.corda.testing.node.NodeTestUtils.ledger;
18+
619
public class ContractTests {
7-
private final MockServices ledgerServices = new MockServices();
820

21+
static private final MockServices ledgerServices = new MockServices();
22+
static private final TestIdentity seller = new TestIdentity(new CordaX500Name("MegaCorp", "London", "GB"));
23+
static private final TestIdentity buyer = new TestIdentity(new CordaX500Name("MiniCorp", "London", "GB"));
24+
25+
26+
//Both the encumbrance and the encumbered state must be added to the transaction
27+
@Test
28+
public void thereMustBeTwoOutputs() {
29+
ledger(ledgerServices, (ledger -> {
30+
ledger.transaction(tx -> {
31+
tx.output(AvatarContract.AVATAR_CONTRACT_ID, new Avatar(seller.getParty(), "1"));
32+
tx.command(ImmutableList.of(seller.getPublicKey()), new AvatarContract.Commands.Create());
33+
tx.fails();
34+
return null;
35+
});
36+
return null;
37+
}));
38+
}
39+
40+
//Specifying time window is mandatory. This is checked in the encumbrance Expiry state.
41+
@Test
42+
public void specifyTimeWindow() {
43+
ledger(ledgerServices, (ledger -> {
44+
ledger.transaction(tx -> {
45+
//this fails as time window is not specified
46+
tx.output(AvatarContract.AVATAR_CONTRACT_ID, new Avatar(seller.getParty(), "1"));
47+
tx.output(ExpiryContract.EXPIRY_CONTRACT_ID,
48+
new Expiry(Instant.now().plus(2, ChronoUnit.MINUTES), "1", seller.getParty()));
49+
tx.command(ImmutableList.of(seller.getPublicKey()), new AvatarContract.Commands.Create());
50+
tx.fails();
51+
52+
//this will pass once we specify time window
53+
tx.timeWindow(Instant.now(), Duration.ofMinutes(1));
54+
return tx.verifies();
55+
});
56+
return null;
57+
}));
58+
}
59+
60+
//For selling, the Expiry of avatar must be greater than the time window
61+
@Test
62+
public void avatarIsRejectedIfItIsExpired() {
63+
ledger(ledgerServices, (ledger -> {
64+
ledger.transaction(tx -> {
65+
tx.output(AvatarContract.AVATAR_CONTRACT_ID, new Avatar(seller.getParty(), "1"));
66+
tx.output(ExpiryContract.EXPIRY_CONTRACT_ID,
67+
new Expiry(Instant.now().plus(2, ChronoUnit.MINUTES), "1", seller.getParty()));
68+
tx.command(ImmutableList.of(seller.getPublicKey()), new AvatarContract.Commands.Create());
69+
tx.timeWindow(Instant.now(), Duration.ofMinutes(3));
70+
tx.fails();
71+
return null;
72+
});
73+
return null;
74+
}));
75+
}
76+
77+
//For selling, the Expiry of avatar must be greater than the time window
978
@Test
10-
public void dummyTest() {
79+
public void expirationDateShouldBeAfterTheTimeWindow() {
80+
ledger(ledgerServices, (ledger -> {
81+
ledger.transaction(tx -> {
82+
tx.output(AvatarContract.AVATAR_CONTRACT_ID, new Avatar(seller.getParty(), "1"));
83+
tx.output(ExpiryContract.EXPIRY_CONTRACT_ID,
84+
new Expiry(Instant.now().plus(3, ChronoUnit.MINUTES), "1", seller.getParty()));
85+
tx.command(ImmutableList.of(seller.getPublicKey()), new AvatarContract.Commands.Create());
86+
tx.timeWindow(Instant.now(), Duration.ofMinutes(2));
87+
tx.verifies();
88+
return null;
89+
});
90+
return null;
91+
}));
1192

1293
}
1394
}

0 commit comments

Comments
 (0)