diff --git a/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java b/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java index caf48df2f4..89fc874e1b 100644 --- a/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java +++ b/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java @@ -25,7 +25,7 @@ import static java.util.Collections.singletonList; import static java.util.Objects.requireNonNull; import static kotlin.test.AssertionsKt.assertEquals; import static net.corda.finance.Currencies.DOLLARS; -import static net.corda.finance.contracts.GetBalances.getCashBalance; +import static net.corda.finance.workflows.GetBalances.getCashBalance; import static net.corda.node.services.Permissions.invokeRpc; import static net.corda.node.services.Permissions.startFlow; import static net.corda.testing.core.TestConstants.ALICE_NAME; diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt index 6763abb814..38bcf1490b 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt @@ -15,8 +15,8 @@ import net.corda.finance.DOLLARS import net.corda.finance.POUNDS import net.corda.finance.USD import net.corda.finance.contracts.asset.Cash -import net.corda.finance.contracts.getCashBalance -import net.corda.finance.contracts.getCashBalances +import net.corda.finance.workflows.getCashBalance +import net.corda.finance.workflows.getCashBalances import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow import net.corda.node.internal.NodeWithInfo diff --git a/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java b/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java index 9527a63b77..77fee26039 100644 --- a/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java +++ b/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java @@ -27,7 +27,7 @@ import java.util.stream.Stream; import static kotlin.test.AssertionsKt.assertEquals; import static kotlin.test.AssertionsKt.fail; -import static net.corda.finance.contracts.GetBalances.getCashBalance; +import static net.corda.finance.workflows.GetBalances.getCashBalance; public class StandaloneCordaRPCJavaClientTest { diff --git a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt index 18884a1ad5..5731368884 100644 --- a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt +++ b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt @@ -20,8 +20,8 @@ import net.corda.finance.POUNDS import net.corda.finance.SWISS_FRANCS import net.corda.finance.USD import net.corda.finance.contracts.asset.Cash -import net.corda.finance.contracts.getCashBalance -import net.corda.finance.contracts.getCashBalances +import net.corda.finance.workflows.getCashBalance +import net.corda.finance.workflows.getCashBalances import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow import net.corda.java.rpc.StandaloneCordaRPCJavaClientTest diff --git a/core/src/test/kotlin/net/corda/core/flows/ReceiveFinalityFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ReceiveFinalityFlowTest.kt index 80b39fccf8..f3eeb05421 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ReceiveFinalityFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ReceiveFinalityFlowTest.kt @@ -8,7 +8,7 @@ import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow import net.corda.finance.GBP import net.corda.finance.POUNDS -import net.corda.finance.contracts.getCashBalance +import net.corda.finance.workflows.getCashBalance import net.corda.finance.flows.CashIssueAndPaymentFlow import net.corda.finance.flows.CashPaymentReceiverFlow import net.corda.node.services.statemachine.StaffedFlowHospital.* diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index e939e7a0b2..7a394eb922 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -11,6 +11,22 @@ Unreleased * Test ``CordaService`` s can be installed on mock nodes using ``UnstartedMockNode.installCordaService``. +* The finance-contracts demo CorDapp has been slimmed down to contain only that which is relevant for contract verification. Everything else + has been moved to the finance-workflows CorDapp: + + * The cash selection logic. ``AbstractCashSelection`` is now in net.corda.finance.contracts.asset so any custom implementations must now be + defined in ``META-INF/services/net.corda.finance.workflows.asset.selection.AbstractCashSelection``. + + * The jackson annotations on ``Expression`` have been removed. You will need to use ``FinanceJSONSupport.registerFinanceJSONMappers`` if + you wish to preserve the JSON format for this class. + + * The various utility methods defined in ``Cash`` for creating cash transactions have been moved to ``net.corda.finance.workflows.asset.CashUtils``. + Similarly with ``CommercialPaperUtils`` and ``ObligationUtils``. + + * Various other utilities such as ``GetBalances` and the test calendar data. + + The only exception to this is ``Interpolator`` and related classes. These are now in the `IRS demo workflows CorDapp `_. + .. _changelog_v4.0: Version 4.0 diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/contract/CommercialPaper.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/contract/CommercialPaper.java index b91ea13b9c..0a3a2d992c 100644 --- a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/contract/CommercialPaper.java +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/contract/CommercialPaper.java @@ -10,7 +10,7 @@ import java.util.List; import static net.corda.core.contracts.ContractsDSL.requireSingleCommand; import static net.corda.core.contracts.ContractsDSL.requireThat; -import static net.corda.finance.utils.StateSumming.sumCashBy; +import static net.corda.finance.contracts.utils.StateSumming.sumCashBy; public class CommercialPaper implements Contract { // DOCSTART 1 diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/contract/TutorialContract.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/contract/TutorialContract.kt index 39c9be245f..db26abf399 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/contract/TutorialContract.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/contract/TutorialContract.kt @@ -8,8 +8,8 @@ import net.corda.core.identity.Party import net.corda.core.node.ServiceHub import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder -import net.corda.finance.contracts.asset.Cash -import net.corda.finance.utils.sumCashBy +import net.corda.finance.contracts.utils.sumCashBy +import net.corda.finance.workflows.asset.CashUtils import net.corda.testing.core.singleIdentityAndCert import java.time.Instant import java.util.* @@ -108,7 +108,7 @@ class CommercialPaper : Contract { @Throws(InsufficientBalanceException::class) fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef, services: ServiceHub) { // Add the cash movement using the states in our vault. - Cash.generateSpend( + CashUtils.generateSpend( services = services, tx = tx, amount = paper.state.data.faceValue.withoutIssuer(), diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/FxTransactionBuildTutorialTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/FxTransactionBuildTutorialTest.kt index 1a0808fe91..96daede1f2 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/FxTransactionBuildTutorialTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/FxTransactionBuildTutorialTest.kt @@ -5,7 +5,7 @@ import net.corda.core.toFuture import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow import net.corda.finance.* -import net.corda.finance.contracts.getCashBalances +import net.corda.finance.workflows.getCashBalances import net.corda.finance.flows.CashIssueFlow import net.corda.testing.core.singleIdentity import net.corda.testing.node.MockNetwork diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/vault/CustomVaultQueryTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/vault/CustomVaultQueryTest.kt index 42c09d29db..b500fa1b52 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/vault/CustomVaultQueryTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/kotlin/vault/CustomVaultQueryTest.kt @@ -10,7 +10,7 @@ import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow import net.corda.docs.kotlin.tutorial.helloworld.IOUFlow import net.corda.finance.* -import net.corda.finance.contracts.getCashBalances +import net.corda.finance.workflows.getCashBalances import net.corda.finance.flows.CashIssueFlow import net.corda.node.services.vault.VaultSchemaV1 import net.corda.testing.core.singleIdentity diff --git a/finance/contracts/build.gradle b/finance/contracts/build.gradle index d296ac46dd..2489c895d3 100644 --- a/finance/contracts/build.gradle +++ b/finance/contracts/build.gradle @@ -11,14 +11,8 @@ apply plugin: 'com.jfrog.artifactory' description 'Corda finance module - contracts' dependencies { - // Note: 3rd party CorDapps should remember to include the relevant Finance CorDapp dependencies using `cordapp` - // cordapp project(':finance:workflows') - // cordapp project(':finance:contracts') cordaCompile project(':core') - // For JSON - compile "com.fasterxml.jackson.core:jackson-databind:${jackson_version}" - testCompile project(':test-utils') testCompile project(path: ':core', configuration: 'testArtifacts') testCompile "junit:junit:$junit_version" diff --git a/finance/contracts/src/main/java/net/corda/finance/contracts/JavaCommercialPaper.java b/finance/contracts/src/main/java/net/corda/finance/contracts/JavaCommercialPaper.java index 0cf5ce0085..0d57ac9694 100644 --- a/finance/contracts/src/main/java/net/corda/finance/contracts/JavaCommercialPaper.java +++ b/finance/contracts/src/main/java/net/corda/finance/contracts/JavaCommercialPaper.java @@ -1,26 +1,16 @@ package net.corda.finance.contracts; -import co.paralleluniverse.fibers.Suspendable; import kotlin.Unit; import net.corda.core.contracts.*; import net.corda.core.crypto.NullKeys.NullPublicKey; import net.corda.core.identity.AbstractParty; import net.corda.core.identity.AnonymousParty; -import net.corda.core.identity.Party; -import net.corda.core.identity.PartyAndCertificate; -import net.corda.core.node.ServiceHub; import net.corda.core.transactions.LedgerTransaction; -import net.corda.core.transactions.TransactionBuilder; -import net.corda.finance.contracts.asset.Cash; -import net.corda.finance.utils.StateSumming; +import net.corda.finance.contracts.utils.StateSumming; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import java.time.Instant; -import java.util.Collections; -import java.util.Currency; -import java.util.Iterator; -import java.util.List; +import java.util.*; import java.util.stream.Collectors; import static net.corda.core.contracts.ContractsDSL.requireSingleCommand; @@ -98,19 +88,15 @@ public class JavaCommercialPaper implements Contract { State state = (State) that; - if (issuance != null ? !issuance.equals(state.issuance) : state.issuance != null) return false; - if (owner != null ? !owner.equals(state.owner) : state.owner != null) return false; - if (faceValue != null ? !faceValue.equals(state.faceValue) : state.faceValue != null) return false; - return maturityDate != null ? maturityDate.equals(state.maturityDate) : state.maturityDate == null; + if (!Objects.equals(issuance, state.issuance)) return false; + if (!Objects.equals(owner, state.owner)) return false; + if (!Objects.equals(faceValue, state.faceValue)) return false; + return Objects.equals(maturityDate, state.maturityDate); } @Override public int hashCode() { - int result = issuance != null ? issuance.hashCode() : 0; - result = 31 * result + (owner != null ? owner.hashCode() : 0); - result = 31 * result + (faceValue != null ? faceValue.hashCode() : 0); - result = 31 * result + (maturityDate != null ? maturityDate.hashCode() : 0); - return result; + return Objects.hash(issuance, owner, faceValue, maturityDate); } State withoutOwner() { @@ -227,32 +213,6 @@ public class JavaCommercialPaper implements Contract { } } - public TransactionBuilder generateIssue(@NotNull PartyAndReference issuance, @NotNull Amount> faceValue, @Nullable Instant maturityDate, @NotNull Party notary, Integer encumbrance) { - State state = new State(issuance, issuance.getParty(), faceValue, maturityDate); - TransactionState output = new TransactionState<>(state, JCP_PROGRAM_ID, notary, encumbrance); - return new TransactionBuilder(notary).withItems(output, new Command<>(new Commands.Issue(), issuance.getParty().getOwningKey())); - } - - public TransactionBuilder generateIssue(@NotNull PartyAndReference issuance, @NotNull Amount> faceValue, @Nullable Instant maturityDate, @NotNull Party notary) { - return generateIssue(issuance, faceValue, maturityDate, notary, null); - } - - @Suspendable - public void generateRedeem(final TransactionBuilder tx, - final StateAndRef paper, - final ServiceHub services, - final PartyAndCertificate ourIdentity) throws InsufficientBalanceException { - Cash.generateSpend(services, tx, Structures.withoutIssuer(paper.getState().getData().getFaceValue()), ourIdentity, paper.getState().getData().getOwner(), Collections.emptySet()); - tx.addInputState(paper); - tx.addCommand(new Command<>(new Commands.Redeem(), paper.getState().getData().getOwner().getOwningKey())); - } - - public void generateMove(TransactionBuilder tx, StateAndRef paper, AbstractParty newOwner) { - tx.addInputState(paper); - tx.addOutputState(new TransactionState<>(new State(paper.getState().getData().getIssuance(), newOwner, paper.getState().getData().getFaceValue(), paper.getState().getData().getMaturityDate()), JCP_PROGRAM_ID, paper.getState().getNotary(), paper.getState().getEncumbrance())); - tx.addCommand(new Command<>(new Commands.Move(), paper.getState().getData().getOwner().getOwningKey())); - } - private static T onlyElementOf(Iterable iterable) { Iterator iter = iterable.iterator(); T item = iter.next(); diff --git a/finance/contracts/src/main/kotlin/net/corda/finance/contracts/BusinessCalendar.kt b/finance/contracts/src/main/kotlin/net/corda/finance/contracts/BusinessCalendar.kt new file mode 100644 index 0000000000..b451de72ba --- /dev/null +++ b/finance/contracts/src/main/kotlin/net/corda/finance/contracts/BusinessCalendar.kt @@ -0,0 +1,129 @@ +package net.corda.finance.contracts + +import net.corda.core.serialization.CordaSerializable +import java.time.DayOfWeek +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import java.util.* +import java.util.Collections.emptySortedSet + +/** + * A business calendar performs date calculations that take into account national holidays and weekends. This is a + * typical feature of financial contracts, in which a business may not want a payment event to fall on a day when + * no staff are around to handle problems. + */ +@CordaSerializable +open class BusinessCalendar(val holidayDates: SortedSet) { + companion object { + @JvmField + val EMPTY = BusinessCalendar(emptySortedSet()) + + @JvmStatic + fun calculateDaysBetween(startDate: LocalDate, + endDate: LocalDate, + dcbYear: DayCountBasisYear, + dcbDay: DayCountBasisDay): Int { + // Right now we are only considering Actual/360 and 30/360 .. We'll do the rest later. + // TODO: The rest. + return when { + dcbDay == DayCountBasisDay.DActual -> (endDate.toEpochDay() - startDate.toEpochDay()).toInt() + dcbDay == DayCountBasisDay.D30 && dcbYear == DayCountBasisYear.Y360 -> ((endDate.year - startDate.year) * 360.0 + (endDate.monthValue - startDate.monthValue) * 30.0 + endDate.dayOfMonth - startDate.dayOfMonth).toInt() + else -> TODO("Can't calculate days using convention $dcbDay / $dcbYear") + } + } + + /** Parses a date of the form YYYY-MM-DD, like 2016-01-10 for 10th Jan. */ + @JvmStatic + fun parseDateFromString(it: String): LocalDate = LocalDate.parse(it, DateTimeFormatter.ISO_LOCAL_DATE) + + /** Calculates an event schedule that moves events around to ensure they fall on working days. */ + @JvmStatic + fun createGenericSchedule(startDate: LocalDate, + period: Frequency, + calendar: BusinessCalendar = EMPTY, + dateRollConvention: DateRollConvention = DateRollConvention.Following, + noOfAdditionalPeriods: Int = Integer.MAX_VALUE, + endDate: LocalDate? = null, + periodOffset: Int? = null): List { + val ret = ArrayList() + var ctr = 0 + var currentDate = startDate + + while (true) { + currentDate = getOffsetDate(currentDate, period) + if (periodOffset == null || periodOffset <= ctr) + ret.add(calendar.applyRollConvention(currentDate, dateRollConvention)) + ctr += 1 + // TODO: Fix addl period logic + if ((ctr > noOfAdditionalPeriods) || (currentDate >= endDate ?: currentDate)) + break + } + return ret + } + + /** + * Calculates the date from @startDate moving forward 'steps' of time size 'period'. Does not apply calendar + * logic / roll conventions. + */ + @JvmStatic + fun getOffsetDate(startDate: LocalDate, period: Frequency, steps: Int = 1): LocalDate { + if (steps == 0) return startDate + return period.offset(startDate, steps.toLong()) + } + } + + operator fun plus(other: BusinessCalendar): BusinessCalendar = BusinessCalendar((this.holidayDates + other.holidayDates).toSortedSet()) + + override fun equals(other: Any?): Boolean = other is BusinessCalendar && this.holidayDates == other.holidayDates + + override fun hashCode(): Int = holidayDates.hashCode() + + open fun isWorkingDay(date: LocalDate): Boolean = + when { + date.dayOfWeek == DayOfWeek.SATURDAY -> false + date.dayOfWeek == DayOfWeek.SUNDAY -> false + holidayDates.contains(date) -> false + else -> true + } + + open fun applyRollConvention(testDate: LocalDate, dateRollConvention: DateRollConvention): LocalDate { + if (dateRollConvention == DateRollConvention.Actual) return testDate + + var direction = dateRollConvention.direction().value + var trialDate = testDate + while (!isWorkingDay(trialDate)) { + trialDate = trialDate.plusDays(direction) + } + + // We've moved to the next working day in the right direction, but if we're using the "modified" date roll + // convention and we've crossed into another month, reverse the direction instead to stay within the month. + // Probably better explained here: http://www.investopedia.com/terms/m/modifiedfollowing.asp + + if (dateRollConvention.isModified && testDate.month != trialDate.month) { + direction = -direction + trialDate = testDate + while (!isWorkingDay(trialDate)) { + trialDate = trialDate.plusDays(direction) + } + } + return trialDate + } + + /** + * Returns a date which is the inbound date plus/minus a given number of business days. + * TODO: Make more efficient if necessary + */ + fun moveBusinessDays(date: LocalDate, direction: DateRollDirection, i: Int): LocalDate { + require(i >= 0){"Days to add/subtract must be positive"} + if (i == 0) return date + var retDate = date + var ctr = 0 + while (ctr < i) { + retDate = retDate.plusDays(direction.value) + if (isWorkingDay(retDate)) ctr++ + } + return retDate + } + + override fun toString(): String = "BusinessCalendar($holidayDates)" +} diff --git a/finance/contracts/src/main/kotlin/net/corda/finance/contracts/CommercialPaper.kt b/finance/contracts/src/main/kotlin/net/corda/finance/contracts/CommercialPaper.kt index 5776137005..1a06c0fb1b 100644 --- a/finance/contracts/src/main/kotlin/net/corda/finance/contracts/CommercialPaper.kt +++ b/finance/contracts/src/main/kotlin/net/corda/finance/contracts/CommercialPaper.kt @@ -1,22 +1,16 @@ package net.corda.finance.contracts -import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.* import net.corda.core.crypto.NullKeys.NULL_PARTY import net.corda.core.crypto.toStringShort import net.corda.core.identity.AbstractParty -import net.corda.core.identity.Party -import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.Emoji -import net.corda.core.node.ServiceHub import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState import net.corda.core.schemas.QueryableState import net.corda.core.transactions.LedgerTransaction -import net.corda.core.transactions.TransactionBuilder -import net.corda.finance.contracts.asset.Cash import net.corda.finance.schemas.CommercialPaperSchemaV1 -import net.corda.finance.utils.sumCashBy +import net.corda.finance.contracts.utils.sumCashBy import java.time.Instant import java.util.* @@ -159,40 +153,4 @@ class CommercialPaper : Contract { } } } - - /** - * Returns a transaction that issues commercial paper, owned by the issuing parties key. Does not update - * an existing transaction because you aren't able to issue multiple pieces of CP in a single transaction - * at the moment: this restriction is not fundamental and may be lifted later. - */ - fun generateIssue(issuance: PartyAndReference, faceValue: Amount>, maturityDate: Instant, - notary: Party): TransactionBuilder { - val state = State(issuance, issuance.party, faceValue, maturityDate) - return TransactionBuilder(notary = notary).withItems(StateAndContract(state, CP_PROGRAM_ID), Command(Commands.Issue(), issuance.party.owningKey)) - } - - /** - * Updates the given partial transaction with an input/output/command to reassign ownership of the paper. - */ - fun generateMove(tx: TransactionBuilder, paper: StateAndRef, newOwner: AbstractParty) { - tx.addInputState(paper) - tx.addOutputState(paper.state.data.withOwner(newOwner), CP_PROGRAM_ID) - tx.addCommand(Commands.Move(), paper.state.data.owner.owningKey) - } - - /** - * Intended to be called by the issuer of some commercial paper, when an owner has notified us that they wish - * to redeem the paper. We must therefore send enough money to the key that owns the paper to satisfy the face - * value, and then ensure the paper is removed from the ledger. - * - * @throws InsufficientBalanceException if the vault doesn't contain enough money to pay the redeemer. - */ - @Throws(InsufficientBalanceException::class) - @Suspendable - fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef, services: ServiceHub, ourIdentity: PartyAndCertificate) { - // Add the cash movement using the states in our vault. - Cash.generateSpend(services, tx, paper.state.data.faceValue.withoutIssuer(), ourIdentity, paper.state.data.owner) - tx.addInputState(paper) - tx.addCommand(Commands.Redeem(), paper.state.data.owner.owningKey) - } } \ No newline at end of file diff --git a/finance/contracts/src/main/kotlin/net/corda/finance/contracts/FinanceTypes.kt b/finance/contracts/src/main/kotlin/net/corda/finance/contracts/FinanceTypes.kt index 849cee24a8..2b88d8f381 100644 --- a/finance/contracts/src/main/kotlin/net/corda/finance/contracts/FinanceTypes.kt +++ b/finance/contracts/src/main/kotlin/net/corda/finance/contracts/FinanceTypes.kt @@ -1,25 +1,14 @@ package net.corda.finance.contracts -import com.fasterxml.jackson.core.JsonGenerator -import com.fasterxml.jackson.core.JsonParser -import com.fasterxml.jackson.databind.DeserializationContext -import com.fasterxml.jackson.databind.JsonDeserializer -import com.fasterxml.jackson.databind.JsonSerializer -import com.fasterxml.jackson.databind.SerializerProvider -import com.fasterxml.jackson.databind.annotation.JsonDeserialize -import com.fasterxml.jackson.databind.annotation.JsonSerialize import net.corda.core.contracts.CommandData import net.corda.core.contracts.LinearState import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.TokenizableAssetInfo -import net.corda.core.flows.FlowException import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.TransactionBuilder import java.math.BigDecimal -import java.time.DayOfWeek import java.time.LocalDate -import java.time.format.DateTimeFormatter import java.util.* //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -41,22 +30,8 @@ data class Fix(val of: FixOf, val value: BigDecimal) : CommandData /** Represents a textual expression of e.g. a formula */ @CordaSerializable -@JsonDeserialize(using = ExpressionDeserializer::class) -@JsonSerialize(using = ExpressionSerializer::class) data class Expression(val expr: String) -object ExpressionSerializer : JsonSerializer() { - override fun serialize(expr: Expression, generator: JsonGenerator, provider: SerializerProvider) { - generator.writeString(expr.expr) - } -} - -object ExpressionDeserializer : JsonDeserializer() { - override fun deserialize(parser: JsonParser, context: DeserializationContext): Expression { - return Expression(parser.text) - } -} - /** Placeholder class for the Tenor datatype - which is a standardised duration of time until maturity */ @CordaSerializable data class Tenor(val name: String) { @@ -194,145 +169,6 @@ enum class Frequency(val annualCompoundCount: Int, val offset: LocalDate.(Long) // TODO: Make Calendar data come from an oracle -/** - * A business calendar performs date calculations that take into account national holidays and weekends. This is a - * typical feature of financial contracts, in which a business may not want a payment event to fall on a day when - * no staff are around to handle problems. - */ -@CordaSerializable -open class BusinessCalendar(val holidayDates: List) { - @CordaSerializable - class UnknownCalendar(name: String) : FlowException("$name not found") - - companion object { - val calendars = listOf("London", "NewYork") - - val TEST_CALENDAR_DATA = calendars.map { - it to BusinessCalendar::class.java.getResourceAsStream("${it}HolidayCalendar.txt").bufferedReader().readText() - }.toMap() - - @JvmStatic - fun calculateDaysBetween(startDate: LocalDate, - endDate: LocalDate, - dcbYear: DayCountBasisYear, - dcbDay: DayCountBasisDay): Int { - // Right now we are only considering Actual/360 and 30/360 .. We'll do the rest later. - // TODO: The rest. - return when { - dcbDay == DayCountBasisDay.DActual -> (endDate.toEpochDay() - startDate.toEpochDay()).toInt() - dcbDay == DayCountBasisDay.D30 && dcbYear == DayCountBasisYear.Y360 -> ((endDate.year - startDate.year) * 360.0 + (endDate.monthValue - startDate.monthValue) * 30.0 + endDate.dayOfMonth - startDate.dayOfMonth).toInt() - else -> TODO("Can't calculate days using convention $dcbDay / $dcbYear") - } - } - - /** Parses a date of the form YYYY-MM-DD, like 2016-01-10 for 10th Jan. */ - @JvmStatic - fun parseDateFromString(it: String): LocalDate = LocalDate.parse(it, DateTimeFormatter.ISO_LOCAL_DATE) - - /** Returns a business calendar that combines all the named holiday calendars into one list of holiday dates. */ - @JvmStatic - fun getInstance(vararg calname: String) = BusinessCalendar( - calname.flatMap { (TEST_CALENDAR_DATA[it] ?: throw UnknownCalendar(it)).split(",") }. - toSet(). - map { parseDateFromString(it) }. - toList().sorted() - ) - - /** Calculates an event schedule that moves events around to ensure they fall on working days. */ - @JvmStatic - fun createGenericSchedule(startDate: LocalDate, - period: Frequency, - calendar: BusinessCalendar = getInstance(), - dateRollConvention: DateRollConvention = DateRollConvention.Following, - noOfAdditionalPeriods: Int = Integer.MAX_VALUE, - endDate: LocalDate? = null, - periodOffset: Int? = null): List { - val ret = ArrayList() - var ctr = 0 - var currentDate = startDate - - while (true) { - currentDate = getOffsetDate(currentDate, period) - if (periodOffset == null || periodOffset <= ctr) - ret.add(calendar.applyRollConvention(currentDate, dateRollConvention)) - ctr += 1 - // TODO: Fix addl period logic - if ((ctr > noOfAdditionalPeriods) || (currentDate >= endDate ?: currentDate)) - break - } - return ret - } - - /** - * Calculates the date from @startDate moving forward 'steps' of time size 'period'. Does not apply calendar - * logic / roll conventions. - */ - @JvmStatic - fun getOffsetDate(startDate: LocalDate, period: Frequency, steps: Int = 1): LocalDate { - if (steps == 0) return startDate - return period.offset(startDate, steps.toLong()) - } - } - - override fun equals(other: Any?): Boolean = if (other is BusinessCalendar) { - /** Note this comparison is OK as we ensure they are sorted in getInstance() */ - this.holidayDates == other.holidayDates - } else { - false - } - - override fun hashCode(): Int { - return this.holidayDates.hashCode() - } - - open fun isWorkingDay(date: LocalDate): Boolean = - when { - date.dayOfWeek == DayOfWeek.SATURDAY -> false - date.dayOfWeek == DayOfWeek.SUNDAY -> false - holidayDates.contains(date) -> false - else -> true - } - - open fun applyRollConvention(testDate: LocalDate, dateRollConvention: DateRollConvention): LocalDate { - if (dateRollConvention == DateRollConvention.Actual) return testDate - - var direction = dateRollConvention.direction().value - var trialDate = testDate - while (!isWorkingDay(trialDate)) { - trialDate = trialDate.plusDays(direction) - } - - // We've moved to the next working day in the right direction, but if we're using the "modified" date roll - // convention and we've crossed into another month, reverse the direction instead to stay within the month. - // Probably better explained here: http://www.investopedia.com/terms/m/modifiedfollowing.asp - - if (dateRollConvention.isModified && testDate.month != trialDate.month) { - direction = -direction - trialDate = testDate - while (!isWorkingDay(trialDate)) { - trialDate = trialDate.plusDays(direction) - } - } - return trialDate - } - - /** - * Returns a date which is the inbound date plus/minus a given number of business days. - * TODO: Make more efficient if necessary - */ - fun moveBusinessDays(date: LocalDate, direction: DateRollDirection, i: Int): LocalDate { - require(i >= 0){"Days to add/subtract must be positive"} - if (i == 0) return date - var retDate = date - var ctr = 0 - while (ctr < i) { - retDate = retDate.plusDays(direction.value) - if (isWorkingDay(retDate)) ctr++ - } - return retDate - } -} - /** A common netting command for contracts whose states can be netted. */ interface NetCommand : CommandData { /** The type of netting to apply, see [NetType] for options. */ diff --git a/finance/contracts/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt b/finance/contracts/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt index d5754a2b68..20d0d78774 100644 --- a/finance/contracts/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt +++ b/finance/contracts/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt @@ -3,25 +3,21 @@ // So the static extension functions get put into a class with a better name than CashKt package net.corda.finance.contracts.asset -import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.* -import net.corda.core.contracts.Amount.Companion.sumOrThrow import net.corda.core.crypto.NullKeys.NULL_PARTY import net.corda.core.crypto.toStringShort import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party -import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.Emoji -import net.corda.core.node.ServiceHub import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState import net.corda.core.schemas.QueryableState import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.finance.schemas.CashSchemaV1 -import net.corda.finance.utils.sumCash -import net.corda.finance.utils.sumCashOrNull -import net.corda.finance.utils.sumCashOrZero +import net.corda.finance.contracts.utils.sumCash +import net.corda.finance.contracts.utils.sumCashOrNull +import net.corda.finance.contracts.utils.sumCashOrZero import java.security.PublicKey import java.util.* @@ -203,137 +199,6 @@ class Cash : OnLedgerAsset() { companion object { const val PROGRAM_ID: ContractClassName = "net.corda.finance.contracts.asset.Cash" - - /** - * Generate a transaction that moves an amount of currency to the given party, and sends any change back to - * sole identity of the calling node. Fails for nodes with multiple identities. - * - * Note: an [Amount] of [Currency] is only fungible for a given Issuer Party within a [FungibleAsset] - * - * @param services The [ServiceHub] to provide access to the database session. - * @param tx A builder, which may contain inputs, outputs and commands already. The relevant components needed - * to move the cash will be added on top. - * @param amount How much currency to send. - * @param to the recipient party. - * @param onlyFromParties if non-null, the asset states will be filtered to only include those issued by the set - * of given parties. This can be useful if the party you're trying to pay has expectations - * about which type of asset claims they are willing to accept. - * @return A [Pair] of the same transaction builder passed in as [tx], and the list of keys that need to sign - * the resulting transaction for it to be valid. - * @throws InsufficientBalanceException when a cash spending transaction fails because - * there is insufficient quantity for a given currency (and optionally set of Issuer Parties). - */ - @JvmStatic - @Throws(InsufficientBalanceException::class) - @Suspendable - @Deprecated("Our identity should be specified", replaceWith = ReplaceWith("generateSpend(services, tx, amount, to, ourIdentity, onlyFromParties)")) - fun generateSpend(services: ServiceHub, - tx: TransactionBuilder, - amount: Amount, - to: AbstractParty, - onlyFromParties: Set = emptySet()) = generateSpend(services, tx, listOf(PartyAndAmount(to, amount)), services.myInfo.legalIdentitiesAndCerts.single(), onlyFromParties) - - /** - * Generate a transaction that moves an amount of currency to the given party. - * - * Note: an [Amount] of [Currency] is only fungible for a given Issuer Party within a [FungibleAsset] - * - * @param services The [ServiceHub] to provide access to the database session. - * @param tx A builder, which may contain inputs, outputs and commands already. The relevant components needed - * to move the cash will be added on top. - * @param amount How much currency to send. - * @param to the recipient party. - * @param ourIdentity well known identity to create a new confidential identity from, for sending change to. - * @param onlyFromParties if non-null, the asset states will be filtered to only include those issued by the set - * of given parties. This can be useful if the party you're trying to pay has expectations - * about which type of asset claims they are willing to accept. - * @return A [Pair] of the same transaction builder passed in as [tx], and the list of keys that need to sign - * the resulting transaction for it to be valid. - * @throws InsufficientBalanceException when a cash spending transaction fails because - * there is insufficient quantity for a given currency (and optionally set of Issuer Parties). - */ - @JvmStatic - @Throws(InsufficientBalanceException::class) - @Suspendable - fun generateSpend(services: ServiceHub, - tx: TransactionBuilder, - amount: Amount, - ourIdentity: PartyAndCertificate, - to: AbstractParty, - onlyFromParties: Set = emptySet()): Pair> { - return generateSpend(services, tx, listOf(PartyAndAmount(to, amount)), ourIdentity, onlyFromParties) - } - - /** - * Generate a transaction that moves money of the given amounts to the recipients specified, and sends any change - * back to sole identity of the calling node. Fails for nodes with multiple identities. - * - * Note: an [Amount] of [Currency] is only fungible for a given Issuer Party within a [FungibleAsset] - * - * @param services The [ServiceHub] to provide access to the database session. - * @param tx A builder, which may contain inputs, outputs and commands already. The relevant components needed - * to move the cash will be added on top. - * @param payments A list of amounts to pay, and the party to send the payment to. - * @param onlyFromParties if non-null, the asset states will be filtered to only include those issued by the set - * of given parties. This can be useful if the party you're trying to pay has expectations - * about which type of asset claims they are willing to accept. - * @return A [Pair] of the same transaction builder passed in as [tx], and the list of keys that need to sign - * the resulting transaction for it to be valid. - * @throws InsufficientBalanceException when a cash spending transaction fails because - * there is insufficient quantity for a given currency (and optionally set of Issuer Parties). - */ - @JvmStatic - @Throws(InsufficientBalanceException::class) - @Suspendable - @Deprecated("Our identity should be specified", replaceWith = ReplaceWith("generateSpend(services, tx, amount, to, ourIdentity, onlyFromParties)")) - fun generateSpend(services: ServiceHub, - tx: TransactionBuilder, - payments: List>, - onlyFromParties: Set = emptySet()) = generateSpend(services, tx, payments, services.myInfo.legalIdentitiesAndCerts.single(), onlyFromParties) - - /** - * Generate a transaction that moves money of the given amounts to the recipients specified. - * - * Note: an [Amount] of [Currency] is only fungible for a given Issuer Party within a [FungibleAsset] - * - * @param services The [ServiceHub] to provide access to the database session. - * @param tx A builder, which may contain inputs, outputs and commands already. The relevant components needed - * to move the cash will be added on top. - * @param payments A list of amounts to pay, and the party to send the payment to. - * @param ourIdentity well known identity to create a new confidential identity from, for sending change to. - * @param onlyFromParties if non-null, the asset states will be filtered to only include those issued by the set - * of given parties. This can be useful if the party you're trying to pay has expectations - * about which type of asset claims they are willing to accept. - * @return A [Pair] of the same transaction builder passed in as [tx], and the list of keys that need to sign - * the resulting transaction for it to be valid. - * @throws InsufficientBalanceException when a cash spending transaction fails because - * there is insufficient quantity for a given currency (and optionally set of Issuer Parties). - */ - @JvmStatic - @Throws(InsufficientBalanceException::class) - @Suspendable - fun generateSpend(services: ServiceHub, - tx: TransactionBuilder, - payments: List>, - ourIdentity: PartyAndCertificate, - onlyFromParties: Set = emptySet()): Pair> { - fun deriveState(txState: TransactionState, amt: Amount>, owner: AbstractParty) - = txState.copy(data = txState.data.copy(amount = amt, owner = owner)) - - // Retrieve unspent and unlocked cash states that meet our spending criteria. - val totalAmount = payments.map { it.amount }.sumOrThrow() - val cashSelection = AbstractCashSelection.getInstance({ services.jdbcSession().metaData }) - val acceptableCoins = cashSelection.unconsumedCashStatesForSpending(services, totalAmount, onlyFromParties, tx.notary, tx.lockId) - val revocationEnabled = false // Revocation is currently unsupported - // Generate a new identity that change will be sent to for confidentiality purposes. This means that a - // third party with a copy of the transaction (such as the notary) cannot identify who the change was - // sent to - val changeIdentity = services.keyManagementService.freshKeyAndCert(ourIdentity, revocationEnabled) - return OnLedgerAsset.generateSpend(tx, payments, acceptableCoins, - changeIdentity.party.anonymise(), - { state, quantity, owner -> deriveState(state, quantity, owner) }, - { Cash().generateMoveCommand() }) - } } } diff --git a/finance/contracts/src/main/kotlin/net/corda/finance/contracts/asset/Obligation.kt b/finance/contracts/src/main/kotlin/net/corda/finance/contracts/asset/Obligation.kt index 27265aa902..586c365b56 100644 --- a/finance/contracts/src/main/kotlin/net/corda/finance/contracts/asset/Obligation.kt +++ b/finance/contracts/src/main/kotlin/net/corda/finance/contracts/asset/Obligation.kt @@ -32,10 +32,10 @@ import net.corda.finance.contracts.NetCommand import net.corda.finance.contracts.NetType import net.corda.finance.contracts.NettableState import net.corda.finance.contracts.asset.Obligation.Lifecycle.NORMAL -import net.corda.finance.utils.sumFungibleOrNull -import net.corda.finance.utils.sumObligations -import net.corda.finance.utils.sumObligationsOrNull -import net.corda.finance.utils.sumObligationsOrZero +import net.corda.finance.contracts.utils.sumFungibleOrNull +import net.corda.finance.contracts.utils.sumObligations +import net.corda.finance.contracts.utils.sumObligationsOrNull +import net.corda.finance.contracts.utils.sumObligationsOrZero import java.security.PublicKey import java.time.Duration import java.time.Instant @@ -479,244 +479,6 @@ class Obligation

: Contract { "the owning keys are a subset of the signing keys" using keysThatSigned.containsAll(owningPubKeys) } } - - /** - * Generate a transaction performing close-out netting of two or more states. - * - * @param signer the party which will sign the transaction. Must be one of the obligor or beneficiary. - * @param inputs two or more states, which must be compatible for bilateral netting (same issuance definitions, - * and same parties involved). - */ - fun generateCloseOutNetting(tx: TransactionBuilder, - signer: AbstractParty, - vararg inputs: StateAndRef>) { - val states = inputs.map { it.state.data } - val netState = states.firstOrNull()?.bilateralNetState - - requireThat { - "at least two states are provided" using (states.size >= 2) - "all states are in the normal lifecycle state " using (states.all { it.lifecycle == Lifecycle.NORMAL }) - "all states must be bilateral nettable" using (states.all { it.bilateralNetState == netState }) - "signer is in the state parties" using (signer in netState!!.partyKeys) - } - - tx.withItems(*inputs) - val out = states.reduce(State

::net) - if (out.quantity > 0L) - tx.addOutputState(out, PROGRAM_ID) - tx.addCommand(Commands.Net(NetType.PAYMENT), signer.owningKey) - } - - /** - * Generate an transaction exiting an obligation from the ledger. - * - * @param tx transaction builder to add states and commands to. - * @param amountIssued the amount to be exited, represented as a quantity of issued currency. - * @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is - * the responsibility of the caller to check that they do not exit funds held by others. - * @return the public keys which must sign the transaction for it to be valid. - */ - @Suppress("unused") - fun generateExit(tx: TransactionBuilder, amountIssued: Amount>>, - assetStates: List>>): Set { - val changeOwner = assetStates.map { it.state.data.owner }.toSet().firstOrNull() ?: throw InsufficientBalanceException(amountIssued) - return OnLedgerAsset.generateExit(tx, amountIssued, assetStates, changeOwner, - deriveState = { state, amount, owner -> state.copy(data = state.data.withNewOwnerAndAmount(amount, owner)) }, - generateMoveCommand = { Commands.Move() }, - generateExitCommand = { amount -> Commands.Exit(amount) } - ) - } - - /** - * Puts together an issuance transaction for the specified currency obligation amount that starts out being owned by - * the given pubkey. - * - * @param tx transaction builder to add states and commands to. - * @param obligor the party who is expected to pay some currency amount to fulfil the obligation (also the owner of - * the obligation). - * @param amount currency amount the obligor is expected to pay. - * @param dueBefore the date on which the obligation is due. The default time tolerance is used (currently this is - * 30 seconds). - * @param beneficiary the party the obligor is expected to pay. - * @param notary the notary for this transaction's outputs. - */ - fun generateCashIssue(tx: TransactionBuilder, - obligor: AbstractParty, - acceptableContract: SecureHash, - amount: Amount>, - dueBefore: Instant, - beneficiary: AbstractParty, - notary: Party) { - val issuanceDef = Terms(NonEmptySet.of(acceptableContract), NonEmptySet.of(amount.token), dueBefore) - OnLedgerAsset.generateIssue(tx, TransactionState(State(Lifecycle.NORMAL, obligor, issuanceDef, amount.quantity, beneficiary), PROGRAM_ID, notary), Commands.Issue()) - } - - /** - * Puts together an issuance transaction for the specified amount that starts out being owned by the given pubkey. - * - * @param tx transaction builder to add states and commands to. - * @param obligor the party who is expected to pay some amount to fulfil the obligation. - * @param issuanceDef the terms of the obligation, including which contracts and underlying assets are acceptable - * forms of payment. - * @param pennies the quantity of the asset (in the smallest normal unit of measurement) owed. - * @param beneficiary the party the obligor is expected to pay. - * @param notary the notary for this transaction's outputs. - */ - fun generateIssue(tx: TransactionBuilder, - obligor: AbstractParty, - issuanceDef: Terms

, - pennies: Long, - beneficiary: AbstractParty, - notary: Party) - = OnLedgerAsset.generateIssue(tx, TransactionState(State(Lifecycle.NORMAL, obligor, issuanceDef, pennies, beneficiary), PROGRAM_ID, notary), Commands.Issue()) - - fun generatePaymentNetting(tx: TransactionBuilder, - issued: Issued>, - notary: Party, - vararg inputs: StateAndRef>) { - val states = inputs.map { it.state.data } - requireThat { - "all states are in the normal lifecycle state " using (states.all { it.lifecycle == Lifecycle.NORMAL }) - } - - tx.withItems(*inputs) - - val groups = states.groupBy { it.multilateralNetState } - val partyLookup = HashMap() - val signers = states.map { it.beneficiary }.union(states.map { it.obligor }).toSet() - - // Create a lookup table of the party that each public key represents. - states.map { it.obligor }.forEach { partyLookup[it.owningKey] = it } - - // Suppress compiler warning as 'groupStates' is an unused variable when destructuring 'groups'. - @Suppress("UNUSED_VARIABLE") - for ((netState, groupStates) in groups) { - // Extract the net balances - val netBalances = netAmountsDue(extractAmountsDue(issued.product, states.asIterable())) - - netBalances - // Convert the balances into obligation state objects - .map { entry -> - State(Lifecycle.NORMAL, entry.key.first, - netState.template, entry.value.quantity, entry.key.second) - } - // Add the new states to the TX - .forEach { tx.addOutputState(it, PROGRAM_ID, notary) } - tx.addCommand(Commands.Net(NetType.PAYMENT), signers.map { it.owningKey }) - } - - } - - /** - * Generate a transaction changing the lifecycle of one or more state objects. - * - * @param statesAndRefs a list of state objects, which MUST all have the same issuance definition. This avoids - * potential complications arising from different deadlines applying to different states. - */ - fun generateSetLifecycle(tx: TransactionBuilder, - statesAndRefs: List>>, - lifecycle: Lifecycle, - notary: Party) { - val states = statesAndRefs.map { it.state.data } - val issuanceDef = getTermsOrThrow(states) - val existingLifecycle = when (lifecycle) { - Lifecycle.DEFAULTED -> Lifecycle.NORMAL - Lifecycle.NORMAL -> Lifecycle.DEFAULTED - } - require(states.all { it.lifecycle == existingLifecycle }) { "initial lifecycle must be $existingLifecycle for all input states" } - - // Produce a new set of states - val groups = statesAndRefs.groupBy { it.state.data.amount.token } - for ((_, stateAndRefs) in groups) { - val partiesUsed = ArrayList() - stateAndRefs.forEach { stateAndRef -> - val outState = stateAndRef.state.data.copy(lifecycle = lifecycle) - tx.addInputState(stateAndRef) - tx.addOutputState(outState, PROGRAM_ID, notary) - partiesUsed.add(stateAndRef.state.data.beneficiary) - } - tx.addCommand(Commands.SetLifecycle(lifecycle), partiesUsed.map { it.owningKey }.distinct()) - } - tx.setTimeWindow(issuanceDef.dueBefore, issuanceDef.timeTolerance) - } - - /** - * @param statesAndRefs a list of state objects, which MUST all have the same aggregate state. This is done as - * only a single settlement command can be present in a transaction, to avoid potential problems with allocating - * assets to different obligation issuances. - * @param assetStatesAndRefs a list of fungible asset state objects, which MUST all be of the same issued product. - * It is strongly encouraged that these all have the same beneficiary. - * @param moveCommand the command used to move the asset state objects to their new owner. - */ - fun generateSettle(tx: TransactionBuilder, - statesAndRefs: Iterable>>, - assetStatesAndRefs: Iterable>>, - moveCommand: MoveCommand, - notary: Party) { - val states = statesAndRefs.map { it.state } - val obligationIssuer = states.first().data.obligor - val obligationOwner = states.first().data.beneficiary - - requireThat { - "all fungible asset states use the same notary" using (assetStatesAndRefs.all { it.state.notary == notary }) - "all obligation states are in the normal state" using (statesAndRefs.all { it.state.data.lifecycle == Lifecycle.NORMAL }) - "all obligation states use the same notary" using (statesAndRefs.all { it.state.notary == notary }) - "all obligation states have the same obligor" using (statesAndRefs.all { it.state.data.obligor == obligationIssuer }) - "all obligation states have the same beneficiary" using (statesAndRefs.all { it.state.data.beneficiary == obligationOwner }) - } - - // TODO: A much better (but more complex) solution would be to have two iterators, one for obligations, - // one for the assets, and step through each in a semi-synced manner. For now however we just bundle all the states - // on each side together - - val issuanceDef = getIssuanceDefinitionOrThrow(statesAndRefs.map { it.state.data }) - val template: Terms

= issuanceDef.product - val obligationTotal: Amount

= Amount(states.map { it.data }.sumObligations

().quantity, template.product) - var obligationRemaining: Amount

= obligationTotal - val assetSigners = HashSet() - - statesAndRefs.forEach { tx.addInputState(it) } - - // Move the assets to the new beneficiary - assetStatesAndRefs.forEach { ref -> - if (obligationRemaining.quantity > 0L) { - tx.addInputState(ref) - - val assetState = ref.state.data - val amount = Amount(assetState.amount.quantity, assetState.amount.token.product) - obligationRemaining -= if (obligationRemaining >= amount) { - tx.addOutputState(assetState.withNewOwnerAndAmount(assetState.amount, obligationOwner), PROGRAM_ID, notary) - amount - } else { - val change = Amount(obligationRemaining.quantity, assetState.amount.token) - // Split the state in two, sending the change back to the previous beneficiary - tx.addOutputState(assetState.withNewOwnerAndAmount(change, obligationOwner), PROGRAM_ID, notary) - tx.addOutputState(assetState.withNewOwnerAndAmount(assetState.amount - change, assetState.owner), PROGRAM_ID, notary) - Amount(0L, obligationRemaining.token) - } - assetSigners.add(assetState.owner) - } - } - - // If we haven't cleared the full obligation, add the remainder as an output - if (obligationRemaining.quantity > 0L) { - tx.addOutputState(State(Lifecycle.NORMAL, obligationIssuer, template, obligationRemaining.quantity, obligationOwner), PROGRAM_ID, notary) - } else { - // Destroy all of the states - } - - // Add the asset move command and obligation settle - tx.addCommand(moveCommand, assetSigners.map { it.owningKey }) - tx.addCommand(Commands.Settle(Amount((obligationTotal - obligationRemaining).quantity, issuanceDef)), obligationIssuer.owningKey) - } - - /** Get the common issuance definition for one or more states, or throw an IllegalArgumentException. */ - private fun getIssuanceDefinitionOrThrow(states: Iterable>): Issued> = - states.map { it.amount.token }.distinct().single() - - /** Get the common issuance definition for one or more states, or throw an IllegalArgumentException. */ - private fun getTermsOrThrow(states: Iterable>) = - states.map { it.template }.distinct().single() } diff --git a/finance/contracts/src/main/kotlin/net/corda/finance/utils/StateSummingUtilities.kt b/finance/contracts/src/main/kotlin/net/corda/finance/contracts/utils/StateSummingUtilities.kt similarity index 98% rename from finance/contracts/src/main/kotlin/net/corda/finance/utils/StateSummingUtilities.kt rename to finance/contracts/src/main/kotlin/net/corda/finance/contracts/utils/StateSummingUtilities.kt index ee155bbaf4..f573428135 100644 --- a/finance/contracts/src/main/kotlin/net/corda/finance/utils/StateSummingUtilities.kt +++ b/finance/contracts/src/main/kotlin/net/corda/finance/contracts/utils/StateSummingUtilities.kt @@ -1,6 +1,6 @@ @file:JvmName("StateSumming") -package net.corda.finance.utils +package net.corda.finance.contracts.utils import net.corda.core.contracts.Amount import net.corda.core.contracts.Amount.Companion.sumOrNull diff --git a/finance/contracts/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt b/finance/contracts/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt index 44d06fb9f1..04a4dfc446 100644 --- a/finance/contracts/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt +++ b/finance/contracts/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt @@ -15,6 +15,8 @@ import net.corda.finance.`issued by` import net.corda.finance.contracts.asset.CASH import net.corda.finance.contracts.asset.Cash import net.corda.finance.contracts.asset.STATE +import net.corda.finance.workflows.asset.CashUtils +import net.corda.finance.workflows.CommercialPaperUtils import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.* import net.corda.testing.dsl.EnforceVerifyOrFail @@ -24,7 +26,6 @@ import net.corda.testing.internal.TEST_TX_TIME import net.corda.testing.internal.vault.VaultFiller import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices -import net.corda.testing.node.internal.MockNetworkParametersStorage import net.corda.testing.node.ledger import net.corda.testing.node.makeTestIdentityService import net.corda.testing.node.transaction @@ -289,7 +290,7 @@ class CommercialPaperTestsGeneric { // MegaCorp™ issues $10,000 of commercial paper, to mature in 30 days, owned initially by itself. val faceValue = 10000.DOLLARS `issued by` dummyCashIssuer.ref(1) val issuance = megaCorpServices.myInfo.singleIdentity().ref(1) - val issueBuilder = CommercialPaper().generateIssue(issuance, faceValue, TEST_TX_TIME + 30.days, dummyNotary.party) + val issueBuilder = CommercialPaperUtils.generateIssue(issuance, faceValue, TEST_TX_TIME + 30.days, dummyNotary.party) issueBuilder.setTimeWindow(TEST_TX_TIME, 30.seconds) val issuePtx = megaCorpServices.signInitialTransaction(issueBuilder) val issueTx = notaryServices.addSignature(issuePtx) @@ -298,8 +299,8 @@ class CommercialPaperTestsGeneric { val moveTX = aliceDatabase.transaction { // Alice pays $9000 to BigCorp to own some of their debt. val builder = TransactionBuilder(dummyNotary.party) - Cash.generateSpend(aliceServices, builder, 9000.DOLLARS, alice.identity, AnonymousParty(megaCorp.publicKey)) - CommercialPaper().generateMove(builder, issueTx.tx.outRef(0), AnonymousParty(alice.keyPair.public)) + CashUtils.generateSpend(aliceServices, builder, 9000.DOLLARS, alice.identity, AnonymousParty(megaCorp.publicKey)) + CommercialPaperUtils.generateMove(builder, issueTx.tx.outRef(0), AnonymousParty(alice.keyPair.public)) val ptx = aliceServices.signInitialTransaction(builder) val ptx2 = megaCorpServices.addSignature(ptx) val stx = notaryServices.addSignature(ptx2) @@ -319,7 +320,7 @@ class CommercialPaperTestsGeneric { fun makeRedeemTX(time: Instant): Pair { val builder = TransactionBuilder(dummyNotary.party) builder.setTimeWindow(time, 30.seconds) - CommercialPaper().generateRedeem(builder, moveTX.tx.outRef(1), megaCorpServices, megaCorpServices.myInfo.singleIdentityAndCert()) + CommercialPaperUtils.generateRedeem(builder, moveTX.tx.outRef(1), megaCorpServices, megaCorpServices.myInfo.singleIdentityAndCert()) val ptx = aliceServices.signInitialTransaction(builder) val ptx2 = megaCorpServices.addSignature(ptx) val stx = notaryServices.addSignature(ptx2) diff --git a/finance/contracts/src/test/kotlin/net/corda/finance/contracts/FinanceTypesTest.kt b/finance/contracts/src/test/kotlin/net/corda/finance/contracts/FinanceTypesTest.kt index 3e5c608a63..da2f1b1912 100644 --- a/finance/contracts/src/test/kotlin/net/corda/finance/contracts/FinanceTypesTest.kt +++ b/finance/contracts/src/test/kotlin/net/corda/finance/contracts/FinanceTypesTest.kt @@ -1,5 +1,6 @@ package net.corda.finance.contracts +import loadTestCalendar import org.junit.Test import java.time.LocalDate import kotlin.test.assertEquals @@ -26,7 +27,7 @@ class FinanceTypesTest { @Test fun `tenor days to maturity adjusted for holiday`() { val tenor = Tenor("1M") - val calendar = BusinessCalendar.getInstance("London") + val calendar = loadTestCalendar("London") val currentDay = LocalDate.of(2016, 2, 27) val maturityDate = currentDay.plusMonths(1).plusDays(2) // 2016-3-27 is a Sunday, next day is a holiday val expectedDaysToMaturity = (maturityDate.toEpochDay() - currentDay.toEpochDay()).toInt() @@ -46,7 +47,13 @@ class FinanceTypesTest { @Test fun `schedule generator 2`() { - val ret = BusinessCalendar.createGenericSchedule(startDate = LocalDate.of(2015, 11, 25), period = Frequency.Monthly, noOfAdditionalPeriods = 3, calendar = BusinessCalendar.getInstance("London"), dateRollConvention = DateRollConvention.Following) + val ret = BusinessCalendar.createGenericSchedule( + startDate = LocalDate.of(2015, 11, 25), + period = Frequency.Monthly, + noOfAdditionalPeriods = 3, + calendar = loadTestCalendar("London"), + dateRollConvention = DateRollConvention.Following + ) // Xmas should not be in the list! assertFalse(LocalDate.of(2015, 12, 25) in ret) println(ret) @@ -55,7 +62,7 @@ class FinanceTypesTest { @Test fun `create a UK calendar`() { - val cal = BusinessCalendar.getInstance("London") + val cal = loadTestCalendar("London") val holdates = cal.holidayDates println(holdates) assertTrue(LocalDate.of(2016, 12, 27) in holdates) // Christmas this year is at the weekend... @@ -63,7 +70,7 @@ class FinanceTypesTest { @Test fun `create a US UK calendar`() { - val cal = BusinessCalendar.getInstance("London", "NewYork") + val cal = loadTestCalendar("London") + loadTestCalendar("NewYork") assertTrue(LocalDate.of(2016, 7, 4) in cal.holidayDates) // The most American of holidays assertTrue(LocalDate.of(2016, 8, 29) in cal.holidayDates) // August Bank Holiday for brits only println("Calendar contains both US and UK holidays") @@ -71,14 +78,14 @@ class FinanceTypesTest { @Test fun `calendar test of modified following`() { - val ldn = BusinessCalendar.getInstance("London") + val ldn = loadTestCalendar("London") val result = ldn.applyRollConvention(LocalDate.of(2016, 12, 25), DateRollConvention.ModifiedFollowing) assertEquals(LocalDate.of(2016, 12, 28), result) } @Test fun `calendar test of modified following pt 2`() { - val ldn = BusinessCalendar.getInstance("London") + val ldn = loadTestCalendar("London") val result = ldn.applyRollConvention(LocalDate.of(2016, 12, 31), DateRollConvention.ModifiedFollowing) assertEquals(LocalDate.of(2016, 12, 30), result) } @@ -86,28 +93,28 @@ class FinanceTypesTest { @Test fun `calendar test of modified previous`() { - val ldn = BusinessCalendar.getInstance("London") + val ldn = loadTestCalendar("London") val result = ldn.applyRollConvention(LocalDate.of(2016, 1, 1), DateRollConvention.ModifiedPrevious) assertEquals(LocalDate.of(2016, 1, 4), result) } @Test fun `calendar test of previous`() { - val ldn = BusinessCalendar.getInstance("London") + val ldn = loadTestCalendar("London") val result = ldn.applyRollConvention(LocalDate.of(2016, 12, 25), DateRollConvention.Previous) assertEquals(LocalDate.of(2016, 12, 23), result) } @Test fun `calendar test of following`() { - val ldn = BusinessCalendar.getInstance("London") + val ldn = loadTestCalendar("London") val result = ldn.applyRollConvention(LocalDate.of(2016, 12, 25), DateRollConvention.Following) assertEquals(LocalDate.of(2016, 12, 28), result) } @Test fun `calendar date advancing`() { - val ldn = BusinessCalendar.getInstance("London") + val ldn = loadTestCalendar("London") val firstDay = LocalDate.of(2015, 12, 20) val expected = mapOf(0 to firstDay, 1 to LocalDate.of(2015, 12, 21), @@ -127,7 +134,7 @@ class FinanceTypesTest { @Test fun `calendar date preceeding`() { - val ldn = BusinessCalendar.getInstance("London") + val ldn = loadTestCalendar("London") val firstDay = LocalDate.of(2015, 12, 31) val expected = mapOf(0 to firstDay, 1 to LocalDate.of(2015, 12, 30), @@ -143,6 +150,5 @@ class FinanceTypesTest { val result = ldn.moveBusinessDays(firstDay, DateRollDirection.BACKWARD, inc) assertEquals(exp, result) } - } } diff --git a/finance/contracts/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt b/finance/contracts/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt index 2bf62af762..b1c7ef250e 100644 --- a/finance/contracts/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt +++ b/finance/contracts/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt @@ -13,10 +13,11 @@ import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.OpaqueBytes import net.corda.finance.* -import net.corda.finance.utils.sumCash -import net.corda.finance.utils.sumCashBy -import net.corda.finance.utils.sumCashOrNull -import net.corda.finance.utils.sumCashOrZero +import net.corda.finance.contracts.utils.sumCash +import net.corda.finance.contracts.utils.sumCashBy +import net.corda.finance.contracts.utils.sumCashOrNull +import net.corda.finance.contracts.utils.sumCashOrZero +import net.corda.finance.workflows.asset.CashUtils import net.corda.node.services.vault.NodeVaultService import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.testing.core.* @@ -527,7 +528,7 @@ class CashTests { val ourIdentity = services.myInfo.singleIdentityAndCert() val tx = TransactionBuilder(dummyNotary.party) database.transaction { - Cash.generateSpend(services, tx, amount, ourIdentity, dest) + CashUtils.generateSpend(services, tx, amount, ourIdentity, dest) } return tx.toWireTransaction(services) } @@ -625,7 +626,7 @@ class CashTests { fun generateSimpleSpendWithParties() { database.transaction { val tx = TransactionBuilder(dummyNotary.party) - Cash.generateSpend(ourServices, tx, 80.DOLLARS, ourServices.myInfo.singleIdentityAndCert(), alice.party, setOf(miniCorp.party)) + CashUtils.generateSpend(ourServices, tx, 80.DOLLARS, ourServices.myInfo.singleIdentityAndCert(), alice.party, setOf(miniCorp.party)) assertEquals(vaultStatesUnconsumed.elementAt(2).ref, tx.inputStates()[0]) } @@ -834,7 +835,7 @@ class CashTests { PartyAndAmount(miniCorpAnonymised, 400.DOLLARS), PartyAndAmount(charlie.party.anonymise(), 150.DOLLARS) ) - Cash.generateSpend(ourServices, tx, payments, ourServices.myInfo.singleIdentityAndCert()) + CashUtils.generateSpend(ourServices, tx, payments, ourServices.myInfo.singleIdentityAndCert()) } val wtx = tx.toWireTransaction(ourServices) fun out(i: Int) = wtx.getOutput(i) as Cash.State @@ -857,14 +858,14 @@ class CashTests { PartyAndAmount(miniCorpAnonymised, 400.DOLLARS), PartyAndAmount(charlie.party.anonymise(), 150.DOLLARS) ) - Cash.generateSpend(ourServices, tx, payments, ourServices.myInfo.singleIdentityAndCert()) + CashUtils.generateSpend(ourServices, tx, payments, ourServices.myInfo.singleIdentityAndCert()) } database.transaction { val payments = listOf( PartyAndAmount(miniCorpAnonymised, 400.POUNDS), PartyAndAmount(charlie.party.anonymise(), 150.POUNDS) ) - Cash.generateSpend(ourServices, tx, payments, ourServices.myInfo.singleIdentityAndCert()) + CashUtils.generateSpend(ourServices, tx, payments, ourServices.myInfo.singleIdentityAndCert()) } val wtx = tx.toWireTransaction(ourServices) diff --git a/finance/contracts/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt b/finance/contracts/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt index 200bff756e..669525a72c 100644 --- a/finance/contracts/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt +++ b/finance/contracts/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt @@ -20,6 +20,7 @@ import net.corda.finance.* import net.corda.finance.contracts.Commodity import net.corda.finance.contracts.NetType import net.corda.finance.contracts.asset.Obligation.Lifecycle +import net.corda.finance.workflows.asset.ObligationUtils import net.corda.node.services.api.IdentityServiceInternal import net.corda.testing.contracts.DummyContract import net.corda.testing.core.* @@ -185,7 +186,7 @@ class ObligationTests { run { // Test generation works. val tx = TransactionBuilder(notary = null).apply { - Obligation().generateIssue(this, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity, + ObligationUtils.generateIssue(this, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity, beneficiary = CHARLIE, notary = DUMMY_NOTARY) }.toWireTransaction(miniCorpServices) assertTrue(tx.inputs.isEmpty()) @@ -257,7 +258,7 @@ class ObligationTests { fun `reject issuance with inputs`() { // Issue some obligation val tx = TransactionBuilder(DUMMY_NOTARY).apply { - Obligation().generateIssue(this, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity, + ObligationUtils.generateIssue(this, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity, beneficiary = MINI_CORP, notary = DUMMY_NOTARY) }.toWireTransaction(miniCorpServices) @@ -265,7 +266,7 @@ class ObligationTests { // Include the previously issued obligation in a new issuance command val ptx = TransactionBuilder(DUMMY_NOTARY) ptx.addInputState(tx.outRef>(0)) - Obligation().generateIssue(ptx, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity, + ObligationUtils.generateIssue(ptx, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity, beneficiary = MINI_CORP, notary = DUMMY_NOTARY) } @@ -275,7 +276,7 @@ class ObligationTests { val obligationAliceToBob = getStateAndRef(oneMillionDollars.OBLIGATION between Pair(ALICE, BOB), Obligation.PROGRAM_ID) val obligationBobToAlice = getStateAndRef(oneMillionDollars.OBLIGATION between Pair(BOB, ALICE), Obligation.PROGRAM_ID) val tx = TransactionBuilder(DUMMY_NOTARY).apply { - Obligation().generateCloseOutNetting(this, ALICE, obligationAliceToBob, obligationBobToAlice) + ObligationUtils.generateCloseOutNetting(this, ALICE, obligationAliceToBob, obligationBobToAlice) }.toWireTransaction(miniCorpServices) assertEquals(0, tx.outputs.size) } @@ -286,7 +287,7 @@ class ObligationTests { val obligationAliceToBob = getStateAndRef((2000000.DOLLARS `issued by` defaultIssuer).OBLIGATION between Pair(ALICE, BOB), Obligation.PROGRAM_ID) val obligationBobToAlice = getStateAndRef(oneMillionDollars.OBLIGATION between Pair(BOB, ALICE), Obligation.PROGRAM_ID) val tx = TransactionBuilder(DUMMY_NOTARY).apply { - Obligation().generateCloseOutNetting(this, ALICE, obligationAliceToBob, obligationBobToAlice) + ObligationUtils.generateCloseOutNetting(this, ALICE, obligationAliceToBob, obligationBobToAlice) }.toWireTransaction(miniCorpServices) assertEquals(1, tx.outputs.size) @@ -300,7 +301,7 @@ class ObligationTests { val obligationAliceToBob = getStateAndRef(oneMillionDollars.OBLIGATION between Pair(ALICE, BOB), Obligation.PROGRAM_ID) val obligationBobToAlice = getStateAndRef(oneMillionDollars.OBLIGATION between Pair(BOB, ALICE), Obligation.PROGRAM_ID) val tx = TransactionBuilder(DUMMY_NOTARY).apply { - Obligation().generatePaymentNetting(this, obligationAliceToBob.state.data.amount.token, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice) + ObligationUtils.generatePaymentNetting(this, obligationAliceToBob.state.data.amount.token, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice) }.toWireTransaction(miniCorpServices) assertEquals(0, tx.outputs.size) } @@ -313,7 +314,7 @@ class ObligationTests { val obligationBobToAlice = getStateAndRef((2000000.DOLLARS `issued by` defaultIssuer).OBLIGATION between Pair(BOB, ALICE), Obligation.PROGRAM_ID) val obligationBobToAliceState = obligationBobToAlice.state.data val tx = TransactionBuilder(DUMMY_NOTARY).apply { - Obligation().generatePaymentNetting(this, obligationAliceToBobState.amount.token, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice) + ObligationUtils.generatePaymentNetting(this, obligationAliceToBobState.amount.token, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice) }.toWireTransaction(miniCorpServices) assertEquals(1, tx.outputs.size) val expected = obligationBobToAliceState.copy(quantity = obligationBobToAliceState.quantity - obligationAliceToBobState.quantity) @@ -336,7 +337,7 @@ class ObligationTests { // Generate a transaction issuing the obligation. var tx = TransactionBuilder(null).apply { val amount = Amount(100, Issued(defaultIssuer, USD)) - Obligation().generateCashIssue(this, ALICE, cashContractBytes.sha256(), amount, dueBefore, + ObligationUtils.generateCashIssue(this, ALICE, cashContractBytes.sha256(), amount, dueBefore, beneficiary = MINI_CORP, notary = DUMMY_NOTARY) } var stx = miniCorpServices.signInitialTransaction(tx) @@ -344,7 +345,7 @@ class ObligationTests { // Now generate a transaction marking the obligation as having defaulted tx = TransactionBuilder(DUMMY_NOTARY).apply { - Obligation().generateSetLifecycle(this, listOf(stateAndRef), Lifecycle.DEFAULTED, DUMMY_NOTARY) + ObligationUtils.generateSetLifecycle(this, listOf(stateAndRef), Lifecycle.DEFAULTED, DUMMY_NOTARY) } var ptx = miniCorpServices.signInitialTransaction(tx, MINI_CORP_PUBKEY) stx = notaryServices.addSignature(ptx) @@ -356,7 +357,7 @@ class ObligationTests { // And set it back stateAndRef = stx.tx.outRef(0) tx = TransactionBuilder(DUMMY_NOTARY).apply { - Obligation().generateSetLifecycle(this, listOf(stateAndRef), Lifecycle.NORMAL, DUMMY_NOTARY) + ObligationUtils.generateSetLifecycle(this, listOf(stateAndRef), Lifecycle.NORMAL, DUMMY_NOTARY) } ptx = miniCorpServices.signInitialTransaction(tx) stx = notaryServices.addSignature(ptx) @@ -374,13 +375,13 @@ class ObligationTests { // Generate a transaction issuing the obligation val obligationTx = TransactionBuilder(null).apply { - Obligation().generateIssue(this, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity, + ObligationUtils.generateIssue(this, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity, beneficiary = MINI_CORP, notary = DUMMY_NOTARY) }.toWireTransaction(miniCorpServices) // Now generate a transaction settling the obligation val settleTx = TransactionBuilder(DUMMY_NOTARY).apply { - Obligation().generateSettle(this, listOf(obligationTx.outRef(0)), listOf(cashTx.outRef(0)), Cash.Commands.Move(), DUMMY_NOTARY) + ObligationUtils.generateSettle(this, listOf(obligationTx.outRef>(0)), listOf(cashTx.outRef(0)), Cash.Commands.Move(), DUMMY_NOTARY) }.toWireTransaction(miniCorpServices) assertEquals(2, settleTx.inputs.size) assertEquals(1, settleTx.outputs.size) diff --git a/finance/workflows/build.gradle b/finance/workflows/build.gradle index 7983d9d195..9b479dbe7f 100644 --- a/finance/workflows/build.gradle +++ b/finance/workflows/build.gradle @@ -31,6 +31,9 @@ dependencies { cordapp project(':confidential-identities') cordapp project(':finance:contracts') + + // For JSON + compile "com.fasterxml.jackson.core:jackson-databind:${jackson_version}" testCompile project(':test-utils') testCompile project(path: ':core', configuration: 'testArtifacts') diff --git a/finance/workflows/src/integration-test/kotlin/net/corda/finance/compat/CashExceptionSerialisationTest.kt b/finance/workflows/src/integration-test/kotlin/net/corda/finance/workflows/CashExceptionSerialisationTest.kt similarity index 69% rename from finance/workflows/src/integration-test/kotlin/net/corda/finance/compat/CashExceptionSerialisationTest.kt rename to finance/workflows/src/integration-test/kotlin/net/corda/finance/workflows/CashExceptionSerialisationTest.kt index cd6efc7f2d..bbcf0dbfeb 100644 --- a/finance/workflows/src/integration-test/kotlin/net/corda/finance/compat/CashExceptionSerialisationTest.kt +++ b/finance/workflows/src/integration-test/kotlin/net/corda/finance/workflows/CashExceptionSerialisationTest.kt @@ -1,9 +1,10 @@ -package net.corda.finance.compat +package net.corda.finance.workflows +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.StartableByRPC import net.corda.core.messaging.startFlow import net.corda.core.utilities.getOrThrow import net.corda.finance.flows.CashException -import net.corda.finance.flows.test.CashExceptionThrowingFlow import net.corda.node.services.Permissions.Companion.all import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.driver @@ -17,11 +18,16 @@ class CashExceptionSerialisationTest { fun `cash exception with a cause can be serialised with AMQP`() { driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) { val node = startNode(rpcUsers = listOf(User("mark", "dadada", setOf(all())))).getOrThrow() - val action = { node.rpc.startFlow(::CashExceptionThrowingFlow).returnValue.getOrThrow() } + val action = { node.rpc.startFlow(CashExceptionSerialisationTest::CashExceptionThrowingFlow).returnValue.getOrThrow() } assertThatThrownBy(action).isInstanceOfSatisfying(CashException::class.java) { thrown -> assertThat(thrown).hasNoCause() assertThat(thrown.stackTrace).isEmpty() } } } + + @StartableByRPC + class CashExceptionThrowingFlow : FlowLogic() { + override fun call(): Unit = throw CashException("BOOM!", IllegalStateException("Nope dude!")) + } } diff --git a/finance/workflows/src/main/kotlin/net/corda/finance/flows/CashExitFlow.kt b/finance/workflows/src/main/kotlin/net/corda/finance/flows/CashExitFlow.kt index f12e73942d..6ab02340ad 100644 --- a/finance/workflows/src/main/kotlin/net/corda/finance/flows/CashExitFlow.kt +++ b/finance/workflows/src/main/kotlin/net/corda/finance/flows/CashExitFlow.kt @@ -13,7 +13,7 @@ import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.ProgressTracker import net.corda.finance.contracts.asset.Cash -import net.corda.finance.contracts.asset.AbstractCashSelection +import net.corda.finance.workflows.asset.selection.AbstractCashSelection import net.corda.finance.flows.AbstractCashFlow.Companion.FINALISING_TX import net.corda.finance.flows.AbstractCashFlow.Companion.GENERATING_TX import net.corda.finance.flows.AbstractCashFlow.Companion.SIGNING_TX diff --git a/finance/workflows/src/main/kotlin/net/corda/finance/flows/CashPaymentFlow.kt b/finance/workflows/src/main/kotlin/net/corda/finance/flows/CashPaymentFlow.kt index 940144ed67..571d427981 100644 --- a/finance/workflows/src/main/kotlin/net/corda/finance/flows/CashPaymentFlow.kt +++ b/finance/workflows/src/main/kotlin/net/corda/finance/flows/CashPaymentFlow.kt @@ -10,11 +10,11 @@ import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.unwrap -import net.corda.finance.contracts.asset.Cash import net.corda.finance.flows.AbstractCashFlow.Companion.FINALISING_TX import net.corda.finance.flows.AbstractCashFlow.Companion.GENERATING_ID import net.corda.finance.flows.AbstractCashFlow.Companion.GENERATING_TX import net.corda.finance.flows.AbstractCashFlow.Companion.SIGNING_TX +import net.corda.finance.workflows.asset.CashUtils import java.util.* /** @@ -35,7 +35,8 @@ open class CashPaymentFlow( val anonymous: Boolean, progressTracker: ProgressTracker, val issuerConstraint: Set = emptySet(), - val notary: Party? = null) : AbstractCashFlow(progressTracker) { + val notary: Party? = null +) : AbstractCashFlow(progressTracker) { /** A straightforward constructor that constructs spends using cash states of any issuer. */ constructor(amount: Amount, recipient: Party) : this(amount, recipient, true, tracker()) @@ -61,7 +62,7 @@ open class CashPaymentFlow( logger.info("Generating spend for: ${builder.lockId}") // TODO: Have some way of restricting this to states the caller controls val (spendTX, keysForSigning) = try { - Cash.generateSpend( + CashUtils.generateSpend( serviceHub, builder, amount, diff --git a/finance/workflows/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt b/finance/workflows/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt index 92ab856e69..2342838d92 100644 --- a/finance/workflows/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt +++ b/finance/workflows/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt @@ -12,8 +12,8 @@ import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.seconds import net.corda.core.utilities.unwrap -import net.corda.finance.contracts.asset.Cash -import net.corda.finance.utils.sumCashBy +import net.corda.finance.contracts.utils.sumCashBy +import net.corda.finance.workflows.asset.CashUtils import java.security.PublicKey import java.util.* @@ -224,7 +224,7 @@ object TwoPartyTradeFlow { val ptx = TransactionBuilder(notary) // Add input and output states for the movement of cash, by using the Cash contract to generate the states - val (tx, cashSigningPubKeys) = Cash.generateSpend(serviceHub, ptx, tradeRequest.price, ourIdentityAndCert, tradeRequest.payToIdentity.party) + val (tx, cashSigningPubKeys) = CashUtils.generateSpend(serviceHub, ptx, tradeRequest.price, ourIdentityAndCert, tradeRequest.payToIdentity.party) // Add inputs/outputs/a command for the movement of the asset. tx.addInputState(assetForSale) diff --git a/finance/workflows/src/main/kotlin/net/corda/finance/workflows/CommercialPaperUtils.kt b/finance/workflows/src/main/kotlin/net/corda/finance/workflows/CommercialPaperUtils.kt new file mode 100644 index 0000000000..64b2bffcb5 --- /dev/null +++ b/finance/workflows/src/main/kotlin/net/corda/finance/workflows/CommercialPaperUtils.kt @@ -0,0 +1,56 @@ +package net.corda.finance.workflows + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.* +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.Party +import net.corda.core.identity.PartyAndCertificate +import net.corda.core.node.ServiceHub +import net.corda.core.transactions.TransactionBuilder +import net.corda.finance.contracts.CommercialPaper +import net.corda.finance.workflows.asset.CashUtils +import java.time.Instant +import java.util.* + +object CommercialPaperUtils { + /** + * Returns a transaction that issues commercial paper, owned by the issuing parties key. Does not update + * an existing transaction because you aren't able to issue multiple pieces of CP in a single transaction + * at the moment: this restriction is not fundamental and may be lifted later. + */ + @JvmStatic + fun generateIssue(issuance: PartyAndReference, faceValue: Amount>, maturityDate: Instant, notary: Party): TransactionBuilder { + val state = CommercialPaper.State(issuance, issuance.party, faceValue, maturityDate) + return TransactionBuilder(notary = notary).withItems( + StateAndContract(state, CommercialPaper.CP_PROGRAM_ID), + Command(CommercialPaper.Commands.Issue(), issuance.party.owningKey) + ) + } + + /** + * Updates the given partial transaction with an input/output/command to reassign ownership of the paper. + */ + @JvmStatic + fun generateMove(tx: TransactionBuilder, paper: StateAndRef, newOwner: AbstractParty) { + tx.addInputState(paper) + tx.addOutputState(paper.state.data.withOwner(newOwner), CommercialPaper.CP_PROGRAM_ID) + tx.addCommand(CommercialPaper.Commands.Move(), paper.state.data.owner.owningKey) + } + + /** + * Intended to be called by the issuer of some commercial paper, when an owner has notified us that they wish + * to redeem the paper. We must therefore send enough money to the key that owns the paper to satisfy the face + * value, and then ensure the paper is removed from the ledger. + * + * @throws InsufficientBalanceException if the vault doesn't contain enough money to pay the redeemer. + */ + @Throws(InsufficientBalanceException::class) + @JvmStatic + @Suspendable + fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef, services: ServiceHub, ourIdentity: PartyAndCertificate) { + // Add the cash movement using the states in our vault. + CashUtils.generateSpend(services, tx, paper.state.data.faceValue.withoutIssuer(), ourIdentity, paper.state.data.owner) + tx.addInputState(paper) + tx.addCommand(CommercialPaper.Commands.Redeem(), paper.state.data.owner.owningKey) + } +} diff --git a/finance/contracts/src/main/kotlin/net/corda/finance/contracts/GetBalances.kt b/finance/workflows/src/main/kotlin/net/corda/finance/workflows/GetBalances.kt similarity index 98% rename from finance/contracts/src/main/kotlin/net/corda/finance/contracts/GetBalances.kt rename to finance/workflows/src/main/kotlin/net/corda/finance/workflows/GetBalances.kt index f844ac2e42..942ba6315f 100644 --- a/finance/contracts/src/main/kotlin/net/corda/finance/contracts/GetBalances.kt +++ b/finance/workflows/src/main/kotlin/net/corda/finance/workflows/GetBalances.kt @@ -1,6 +1,6 @@ @file:JvmName("GetBalances") -package net.corda.finance.contracts +package net.corda.finance.workflows import net.corda.core.contracts.Amount import net.corda.core.contracts.FungibleAsset diff --git a/finance/workflows/src/main/kotlin/net/corda/finance/workflows/asset/CashUtils.kt b/finance/workflows/src/main/kotlin/net/corda/finance/workflows/asset/CashUtils.kt new file mode 100644 index 0000000000..dffb08bbf2 --- /dev/null +++ b/finance/workflows/src/main/kotlin/net/corda/finance/workflows/asset/CashUtils.kt @@ -0,0 +1,157 @@ +package net.corda.finance.workflows.asset + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.* +import net.corda.core.contracts.Amount.Companion.sumOrThrow +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.PartyAndCertificate +import net.corda.core.node.ServiceHub +import net.corda.core.transactions.TransactionBuilder +import net.corda.finance.contracts.asset.Cash +import net.corda.finance.contracts.asset.OnLedgerAsset +import net.corda.finance.contracts.asset.PartyAndAmount +import net.corda.finance.workflows.asset.selection.AbstractCashSelection +import java.security.PublicKey +import java.util.* + +object CashUtils { + /** + * Generate a transaction that moves an amount of currency to the given party, and sends any change back to + * sole identity of the calling node. Fails for nodes with multiple identities. + * + * Note: an [Amount] of [Currency] is only fungible for a given Issuer Party within a [FungibleAsset] + * + * @param services The [ServiceHub] to provide access to the database session. + * @param tx A builder, which may contain inputs, outputs and commands already. The relevant components needed + * to move the cash will be added on top. + * @param amount How much currency to send. + * @param to the recipient party. + * @param onlyFromParties if non-null, the asset states will be filtered to only include those issued by the set + * of given parties. This can be useful if the party you're trying to pay has expectations + * about which type of asset claims they are willing to accept. + * @return A [Pair] of the same transaction builder passed in as [tx], and the list of keys that need to sign + * the resulting transaction for it to be valid. + * @throws InsufficientBalanceException when a cash spending transaction fails because + * there is insufficient quantity for a given currency (and optionally set of Issuer Parties). + */ + @JvmStatic + @Throws(InsufficientBalanceException::class) + @Suspendable + @Deprecated("Our identity should be specified", replaceWith = ReplaceWith("generateSpend(services, tx, amount, to, ourIdentity, onlyFromParties)")) + fun generateSpend(services: ServiceHub, + tx: TransactionBuilder, + amount: Amount, + to: AbstractParty, + onlyFromParties: Set = emptySet()): Pair> { + return generateSpend(services, tx, listOf(PartyAndAmount(to, amount)), services.myInfo.legalIdentitiesAndCerts.single(), onlyFromParties) + } + + /** + * Generate a transaction that moves an amount of currency to the given party. + * + * Note: an [Amount] of [Currency] is only fungible for a given Issuer Party within a [FungibleAsset] + * + * @param services The [ServiceHub] to provide access to the database session. + * @param tx A builder, which may contain inputs, outputs and commands already. The relevant components needed + * to move the cash will be added on top. + * @param amount How much currency to send. + * @param to the recipient party. + * @param ourIdentity well known identity to create a new confidential identity from, for sending change to. + * @param onlyFromParties if non-null, the asset states will be filtered to only include those issued by the set + * of given parties. This can be useful if the party you're trying to pay has expectations + * about which type of asset claims they are willing to accept. + * @return A [Pair] of the same transaction builder passed in as [tx], and the list of keys that need to sign + * the resulting transaction for it to be valid. + * @throws InsufficientBalanceException when a cash spending transaction fails because + * there is insufficient quantity for a given currency (and optionally set of Issuer Parties). + */ + @JvmStatic + @Throws(InsufficientBalanceException::class) + @Suspendable + fun generateSpend(services: ServiceHub, + tx: TransactionBuilder, + amount: Amount, + ourIdentity: PartyAndCertificate, + to: AbstractParty, + onlyFromParties: Set = emptySet()): Pair> { + return generateSpend(services, tx, listOf(PartyAndAmount(to, amount)), ourIdentity, onlyFromParties) + } + + /** + * Generate a transaction that moves money of the given amounts to the recipients specified, and sends any change + * back to sole identity of the calling node. Fails for nodes with multiple identities. + * + * Note: an [Amount] of [Currency] is only fungible for a given Issuer Party within a [FungibleAsset] + * + * @param services The [ServiceHub] to provide access to the database session. + * @param tx A builder, which may contain inputs, outputs and commands already. The relevant components needed + * to move the cash will be added on top. + * @param payments A list of amounts to pay, and the party to send the payment to. + * @param onlyFromParties if non-null, the asset states will be filtered to only include those issued by the set + * of given parties. This can be useful if the party you're trying to pay has expectations + * about which type of asset claims they are willing to accept. + * @return A [Pair] of the same transaction builder passed in as [tx], and the list of keys that need to sign + * the resulting transaction for it to be valid. + * @throws InsufficientBalanceException when a cash spending transaction fails because + * there is insufficient quantity for a given currency (and optionally set of Issuer Parties). + */ + @JvmStatic + @Throws(InsufficientBalanceException::class) + @Suspendable + @Deprecated("Our identity should be specified", replaceWith = ReplaceWith("generateSpend(services, tx, amount, to, ourIdentity, onlyFromParties)")) + fun generateSpend(services: ServiceHub, + tx: TransactionBuilder, + payments: List>, + onlyFromParties: Set = emptySet()): Pair> { + return generateSpend(services, tx, payments, services.myInfo.legalIdentitiesAndCerts.single(), onlyFromParties) + } + + /** + * Generate a transaction that moves money of the given amounts to the recipients specified. + * + * Note: an [Amount] of [Currency] is only fungible for a given Issuer Party within a [FungibleAsset] + * + * @param services The [ServiceHub] to provide access to the database session. + * @param tx A builder, which may contain inputs, outputs and commands already. The relevant components needed + * to move the cash will be added on top. + * @param payments A list of amounts to pay, and the party to send the payment to. + * @param ourIdentity well known identity to create a new confidential identity from, for sending change to. + * @param onlyFromParties if non-null, the asset states will be filtered to only include those issued by the set + * of given parties. This can be useful if the party you're trying to pay has expectations + * about which type of asset claims they are willing to accept. + * @return A [Pair] of the same transaction builder passed in as [tx], and the list of keys that need to sign + * the resulting transaction for it to be valid. + * @throws InsufficientBalanceException when a cash spending transaction fails because + * there is insufficient quantity for a given currency (and optionally set of Issuer Parties). + */ + @JvmStatic + @Throws(InsufficientBalanceException::class) + @Suspendable + fun generateSpend(services: ServiceHub, + tx: TransactionBuilder, + payments: List>, + ourIdentity: PartyAndCertificate, + onlyFromParties: Set = emptySet()): Pair> { + fun deriveState(txState: TransactionState, amt: Amount>, owner: AbstractParty): TransactionState { + return txState.copy(data = txState.data.copy(amount = amt, owner = owner)) + } + + // Retrieve unspent and unlocked cash states that meet our spending criteria. + val totalAmount = payments.map { it.amount }.sumOrThrow() + val cashSelection = AbstractCashSelection.getInstance { services.jdbcSession().metaData } + val acceptableCoins = cashSelection.unconsumedCashStatesForSpending(services, totalAmount, onlyFromParties, tx.notary, tx.lockId) + val revocationEnabled = false // Revocation is currently unsupported + // Generate a new identity that change will be sent to for confidentiality purposes. This means that a + // third party with a copy of the transaction (such as the notary) cannot identify who the change was + // sent to + val changeIdentity = services.keyManagementService.freshKeyAndCert(ourIdentity, revocationEnabled) + return OnLedgerAsset.generateSpend( + tx, + payments, + acceptableCoins, + changeIdentity.party.anonymise(), + ::deriveState, + Cash()::generateMoveCommand + ) + } +} diff --git a/finance/workflows/src/main/kotlin/net/corda/finance/workflows/asset/ObligationUtils.kt b/finance/workflows/src/main/kotlin/net/corda/finance/workflows/asset/ObligationUtils.kt new file mode 100644 index 0000000000..6783abfac4 --- /dev/null +++ b/finance/workflows/src/main/kotlin/net/corda/finance/workflows/asset/ObligationUtils.kt @@ -0,0 +1,278 @@ +package net.corda.finance.workflows.asset + +import net.corda.core.contracts.* +import net.corda.core.crypto.SecureHash +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.Party +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.NonEmptySet +import net.corda.finance.contracts.NetType +import net.corda.finance.contracts.asset.Obligation +import net.corda.finance.contracts.asset.OnLedgerAsset +import net.corda.finance.contracts.asset.extractAmountsDue +import net.corda.finance.contracts.asset.netAmountsDue +import net.corda.finance.contracts.utils.sumObligations +import java.security.PublicKey +import java.time.Instant +import java.util.* + +object ObligationUtils { + /** + * Puts together an issuance transaction for the specified currency obligation amount that starts out being owned by + * the given pubkey. + * + * @param tx transaction builder to add states and commands to. + * @param obligor the party who is expected to pay some currency amount to fulfil the obligation (also the owner of + * the obligation). + * @param amount currency amount the obligor is expected to pay. + * @param dueBefore the date on which the obligation is due. The default time tolerance is used (currently this is + * 30 seconds). + * @param beneficiary the party the obligor is expected to pay. + * @param notary the notary for this transaction's outputs. + */ + @JvmStatic + fun generateCashIssue(tx: TransactionBuilder, + obligor: AbstractParty, + acceptableContract: SecureHash, + amount: Amount>, + dueBefore: Instant, + beneficiary: AbstractParty, + notary: Party) { + val issuanceDef = Obligation.Terms(NonEmptySet.of(acceptableContract), NonEmptySet.of(amount.token), dueBefore) + OnLedgerAsset.generateIssue( + tx, + TransactionState( + Obligation.State(Obligation.Lifecycle.NORMAL, obligor, issuanceDef, amount.quantity, beneficiary), + Obligation.PROGRAM_ID, + notary + ), + Obligation.Commands.Issue() + ) + } + + /** + * Puts together an issuance transaction for the specified amount that starts out being owned by the given pubkey. + * + * @param tx transaction builder to add states and commands to. + * @param obligor the party who is expected to pay some amount to fulfil the obligation. + * @param issuanceDef the terms of the obligation, including which contracts and underlying assets are acceptable + * forms of payment. + * @param pennies the quantity of the asset (in the smallest normal unit of measurement) owed. + * @param beneficiary the party the obligor is expected to pay. + * @param notary the notary for this transaction's outputs. + */ + fun

generateIssue(tx: TransactionBuilder, + obligor: AbstractParty, + issuanceDef: Obligation.Terms

, + pennies: Long, + beneficiary: AbstractParty, + notary: Party): Set { + return OnLedgerAsset.generateIssue( + tx, + TransactionState( + Obligation.State(Obligation.Lifecycle.NORMAL, obligor, issuanceDef, pennies, beneficiary), + Obligation.PROGRAM_ID, + notary + ), + Obligation.Commands.Issue() + ) + } + + /** + * Generate a transaction performing close-out netting of two or more states. + * + * @param signer the party which will sign the transaction. Must be one of the obligor or beneficiary. + * @param inputs two or more states, which must be compatible for bilateral netting (same issuance definitions, + * and same parties involved). + */ + @JvmStatic + fun

generateCloseOutNetting(tx: TransactionBuilder, signer: AbstractParty, vararg inputs: StateAndRef>) { + val states = inputs.map { it.state.data } + val netState = states.firstOrNull()?.bilateralNetState + + requireThat { + "at least two states are provided" using (states.size >= 2) + "all states are in the normal lifecycle state " using (states.all { it.lifecycle == Obligation.Lifecycle.NORMAL }) + "all states must be bilateral nettable" using (states.all { it.bilateralNetState == netState }) + "signer is in the state parties" using (signer in netState!!.partyKeys) + } + + tx.withItems(*inputs) + val out = states.reduce(Obligation.State

::net) + if (out.quantity > 0L) { + tx.addOutputState(out, Obligation.PROGRAM_ID) + } + tx.addCommand(Obligation.Commands.Net(NetType.PAYMENT), signer.owningKey) + } + + @JvmStatic + fun

generatePaymentNetting(tx: TransactionBuilder, + issued: Issued>, + notary: Party, + vararg inputs: StateAndRef>) { + val states = inputs.map { it.state.data } + requireThat { + "all states are in the normal lifecycle state " using (states.all { it.lifecycle == Obligation.Lifecycle.NORMAL }) + } + + tx.withItems(*inputs) + + val groups = states.groupBy { it.multilateralNetState } + val partyLookup = HashMap() + val signers = states.map { it.beneficiary }.union(states.map { it.obligor }).toSet() + + // Create a lookup table of the party that each public key represents. + states.map { it.obligor }.forEach { partyLookup[it.owningKey] = it } + + // Suppress compiler warning as 'groupStates' is an unused variable when destructuring 'groups'. + @Suppress("UNUSED_VARIABLE") + for ((netState, groupStates) in groups) { + // Extract the net balances + val netBalances = netAmountsDue(extractAmountsDue(issued.product, states.asIterable())) + + netBalances + // Convert the balances into obligation state objects + .map { entry -> + Obligation.State(Obligation.Lifecycle.NORMAL, entry.key.first, + netState.template, entry.value.quantity, entry.key.second) + } + // Add the new states to the TX + .forEach { tx.addOutputState(it, Obligation.PROGRAM_ID, notary) } + tx.addCommand(Obligation.Commands.Net(NetType.PAYMENT), signers.map { it.owningKey }) + } + } + + /** + * Generate a transaction changing the lifecycle of one or more state objects. + * + * @param statesAndRefs a list of state objects, which MUST all have the same issuance definition. This avoids + * potential complications arising from different deadlines applying to different states. + */ + @JvmStatic + fun

generateSetLifecycle(tx: TransactionBuilder, + statesAndRefs: List>>, + lifecycle: Obligation.Lifecycle, + notary: Party) { + val states = statesAndRefs.map { it.state.data } + val issuanceDef = getTermsOrThrow(states) + val existingLifecycle = when (lifecycle) { + Obligation.Lifecycle.DEFAULTED -> Obligation.Lifecycle.NORMAL + Obligation.Lifecycle.NORMAL -> Obligation.Lifecycle.DEFAULTED + } + require(states.all { it.lifecycle == existingLifecycle }) { "initial lifecycle must be $existingLifecycle for all input states" } + + // Produce a new set of states + val groups = statesAndRefs.groupBy { it.state.data.amount.token } + for ((_, stateAndRefs) in groups) { + val partiesUsed = ArrayList() + stateAndRefs.forEach { stateAndRef -> + val outState = stateAndRef.state.data.copy(lifecycle = lifecycle) + tx.addInputState(stateAndRef) + tx.addOutputState(outState, Obligation.PROGRAM_ID, notary) + partiesUsed.add(stateAndRef.state.data.beneficiary) + } + tx.addCommand(Obligation.Commands.SetLifecycle(lifecycle), partiesUsed.map { it.owningKey }.distinct()) + } + tx.setTimeWindow(issuanceDef.dueBefore, issuanceDef.timeTolerance) + } + + /** Get the common issuance definition for one or more states, or throw an IllegalArgumentException. */ + private fun

getTermsOrThrow(states: Iterable>) = states.map { it.template }.distinct().single() + + /** + * @param statesAndRefs a list of state objects, which MUST all have the same aggregate state. This is done as + * only a single settlement command can be present in a transaction, to avoid potential problems with allocating + * assets to different obligation issuances. + * @param assetStatesAndRefs a list of fungible asset state objects, which MUST all be of the same issued product. + * It is strongly encouraged that these all have the same beneficiary. + * @param moveCommand the command used to move the asset state objects to their new owner. + */ + @JvmStatic + fun

generateSettle(tx: TransactionBuilder, + statesAndRefs: Iterable>>, + assetStatesAndRefs: Iterable>>, + moveCommand: MoveCommand, + notary: Party) { + val states = statesAndRefs.map { it.state } + val obligationIssuer = states.first().data.obligor + val obligationOwner = states.first().data.beneficiary + + requireThat { + "all fungible asset states use the same notary" using (assetStatesAndRefs.all { it.state.notary == notary }) + "all obligation states are in the normal state" using (statesAndRefs.all { it.state.data.lifecycle == Obligation.Lifecycle.NORMAL }) + "all obligation states use the same notary" using (statesAndRefs.all { it.state.notary == notary }) + "all obligation states have the same obligor" using (statesAndRefs.all { it.state.data.obligor == obligationIssuer }) + "all obligation states have the same beneficiary" using (statesAndRefs.all { it.state.data.beneficiary == obligationOwner }) + } + + // TODO: A much better (but more complex) solution would be to have two iterators, one for obligations, + // one for the assets, and step through each in a semi-synced manner. For now however we just bundle all the states + // on each side together + + val issuanceDef = getIssuanceDefinitionOrThrow(statesAndRefs.map { it.state.data }) + val template: Obligation.Terms

= issuanceDef.product + val obligationTotal: Amount

= Amount(states.map { it.data }.sumObligations

().quantity, template.product) + var obligationRemaining: Amount

= obligationTotal + val assetSigners = HashSet() + + statesAndRefs.forEach { tx.addInputState(it) } + + // Move the assets to the new beneficiary + assetStatesAndRefs.forEach { ref -> + if (obligationRemaining.quantity > 0L) { + tx.addInputState(ref) + + val assetState = ref.state.data + val amount = Amount(assetState.amount.quantity, assetState.amount.token.product) + obligationRemaining -= if (obligationRemaining >= amount) { + tx.addOutputState(assetState.withNewOwnerAndAmount(assetState.amount, obligationOwner), Obligation.PROGRAM_ID, notary) + amount + } else { + val change = Amount(obligationRemaining.quantity, assetState.amount.token) + // Split the state in two, sending the change back to the previous beneficiary + tx.addOutputState(assetState.withNewOwnerAndAmount(change, obligationOwner), Obligation.PROGRAM_ID, notary) + tx.addOutputState(assetState.withNewOwnerAndAmount(assetState.amount - change, assetState.owner), Obligation.PROGRAM_ID, notary) + Amount(0L, obligationRemaining.token) + } + assetSigners.add(assetState.owner) + } + } + + // If we haven't cleared the full obligation, add the remainder as an output + if (obligationRemaining.quantity > 0L) { + tx.addOutputState(Obligation.State(Obligation.Lifecycle.NORMAL, obligationIssuer, template, obligationRemaining.quantity, obligationOwner), Obligation.PROGRAM_ID, notary) + } else { + // Destroy all of the states + } + + // Add the asset move command and obligation settle + tx.addCommand(moveCommand, assetSigners.map { it.owningKey }) + tx.addCommand(Obligation.Commands.Settle(Amount((obligationTotal - obligationRemaining).quantity, issuanceDef)), obligationIssuer.owningKey) + } + + /** Get the common issuance definition for one or more states, or throw an IllegalArgumentException. */ + private fun

getIssuanceDefinitionOrThrow(states: Iterable>): Issued> { + return states.map { it.amount.token }.distinct().single() + } + + /** + * Generate an transaction exiting an obligation from the ledger. + * + * @param tx transaction builder to add states and commands to. + * @param amountIssued the amount to be exited, represented as a quantity of issued currency. + * @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is + * the responsibility of the caller to check that they do not exit funds held by others. + * @return the public keys which must sign the transaction for it to be valid. + */ + @JvmStatic + fun

generateExit(tx: TransactionBuilder, + amountIssued: Amount>>, + assetStates: List>>): Set { + val changeOwner = assetStates.map { it.state.data.owner }.toSet().firstOrNull() ?: throw InsufficientBalanceException(amountIssued) + return OnLedgerAsset.generateExit(tx, amountIssued, assetStates, changeOwner, + deriveState = { state, amount, owner -> state.copy(data = state.data.withNewOwnerAndAmount(amount, owner)) }, + generateMoveCommand = { Obligation.Commands.Move() }, + generateExitCommand = { amount -> Obligation.Commands.Exit(amount) } + ) + } +} \ No newline at end of file diff --git a/finance/contracts/src/main/kotlin/net/corda/finance/contracts/asset/AbstractCashSelection.kt b/finance/workflows/src/main/kotlin/net/corda/finance/workflows/asset/selection/AbstractCashSelection.kt similarity index 98% rename from finance/contracts/src/main/kotlin/net/corda/finance/contracts/asset/AbstractCashSelection.kt rename to finance/workflows/src/main/kotlin/net/corda/finance/workflows/asset/selection/AbstractCashSelection.kt index 54fae6eafe..1a2d6ca5b2 100644 --- a/finance/contracts/src/main/kotlin/net/corda/finance/contracts/asset/AbstractCashSelection.kt +++ b/finance/workflows/src/main/kotlin/net/corda/finance/workflows/asset/selection/AbstractCashSelection.kt @@ -1,4 +1,4 @@ -package net.corda.finance.contracts.asset +package net.corda.finance.workflows.asset.selection import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.Amount @@ -12,6 +12,7 @@ import net.corda.core.internal.uncheckedCast import net.corda.core.node.ServiceHub import net.corda.core.node.services.StatesNotAvailableException import net.corda.core.utilities.* +import net.corda.finance.contracts.asset.Cash import java.sql.Connection import java.sql.DatabaseMetaData import java.sql.ResultSet @@ -22,7 +23,7 @@ import java.util.concurrent.atomic.AtomicReference * Pluggable interface to allow for different cash selection provider implementations * Default implementation in finance workflow module uses H2 database and a custom function within H2 to perform aggregation. * Custom implementations must implement this interface and declare their implementation in - * META-INF/services/net.corda.contracts.asset.CashSelection + * `META-INF/services/net.corda.finance.workflows.asset.selection.AbstractCashSelection`. */ // TODO: make parameters configurable when we get CorDapp configuration. abstract class AbstractCashSelection(private val maxRetries : Int = 8, private val retrySleep : Int = 100, diff --git a/finance/workflows/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionH2Impl.kt b/finance/workflows/src/main/kotlin/net/corda/finance/workflows/asset/selection/CashSelectionH2Impl.kt similarity index 96% rename from finance/workflows/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionH2Impl.kt rename to finance/workflows/src/main/kotlin/net/corda/finance/workflows/asset/selection/CashSelectionH2Impl.kt index bb8121fe61..81c7c55e17 100644 --- a/finance/workflows/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionH2Impl.kt +++ b/finance/workflows/src/main/kotlin/net/corda/finance/workflows/asset/selection/CashSelectionH2Impl.kt @@ -1,4 +1,4 @@ -package net.corda.finance.contracts.asset.cash.selection +package net.corda.finance.workflows.asset.selection import net.corda.core.contracts.Amount import net.corda.core.crypto.toStringShort @@ -7,7 +7,6 @@ import net.corda.core.identity.Party import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug -import net.corda.finance.contracts.asset.AbstractCashSelection import java.sql.Connection import java.sql.DatabaseMetaData import java.sql.ResultSet diff --git a/finance/workflows/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionPostgreSQLImpl.kt b/finance/workflows/src/main/kotlin/net/corda/finance/workflows/asset/selection/CashSelectionPostgreSQLImpl.kt similarity index 97% rename from finance/workflows/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionPostgreSQLImpl.kt rename to finance/workflows/src/main/kotlin/net/corda/finance/workflows/asset/selection/CashSelectionPostgreSQLImpl.kt index 69b51c5e54..21bdcc64b5 100644 --- a/finance/workflows/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionPostgreSQLImpl.kt +++ b/finance/workflows/src/main/kotlin/net/corda/finance/workflows/asset/selection/CashSelectionPostgreSQLImpl.kt @@ -1,4 +1,4 @@ -package net.corda.finance.contracts.asset.cash.selection +package net.corda.finance.workflows.asset.selection import net.corda.core.contracts.Amount import net.corda.core.crypto.toStringShort @@ -7,7 +7,6 @@ import net.corda.core.identity.Party import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug -import net.corda.finance.contracts.asset.AbstractCashSelection import java.sql.Connection import java.sql.DatabaseMetaData import java.sql.ResultSet diff --git a/finance/workflows/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionSQLServerImpl.kt b/finance/workflows/src/main/kotlin/net/corda/finance/workflows/asset/selection/CashSelectionSQLServerImpl.kt similarity index 97% rename from finance/workflows/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionSQLServerImpl.kt rename to finance/workflows/src/main/kotlin/net/corda/finance/workflows/asset/selection/CashSelectionSQLServerImpl.kt index 75a9e03c16..ecc8bc278e 100644 --- a/finance/workflows/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionSQLServerImpl.kt +++ b/finance/workflows/src/main/kotlin/net/corda/finance/workflows/asset/selection/CashSelectionSQLServerImpl.kt @@ -1,4 +1,4 @@ -package net.corda.finance.contracts.asset.cash.selection +package net.corda.finance.workflows.asset.selection import net.corda.core.contracts.Amount import net.corda.core.crypto.toStringShort @@ -7,7 +7,6 @@ import net.corda.core.identity.Party import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug -import net.corda.finance.contracts.asset.AbstractCashSelection import java.sql.Connection import java.sql.DatabaseMetaData import java.sql.ResultSet diff --git a/finance/workflows/src/main/kotlin/net/corda/finance/plugin/FinanceJSONSupport.kt b/finance/workflows/src/main/kotlin/net/corda/finance/workflows/plugin/FinanceJSONSupport.kt similarity index 62% rename from finance/workflows/src/main/kotlin/net/corda/finance/plugin/FinanceJSONSupport.kt rename to finance/workflows/src/main/kotlin/net/corda/finance/workflows/plugin/FinanceJSONSupport.kt index 13df7822f0..c077b50bb1 100644 --- a/finance/workflows/src/main/kotlin/net/corda/finance/plugin/FinanceJSONSupport.kt +++ b/finance/workflows/src/main/kotlin/net/corda/finance/workflows/plugin/FinanceJSONSupport.kt @@ -1,31 +1,37 @@ @file:JvmName("FinanceJSONSupport") -package net.corda.finance.plugin +package net.corda.finance.workflows.plugin +import TEST_CALENDAR_NAMES import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.core.JsonParseException import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.databind.* import com.fasterxml.jackson.databind.deser.std.StringArrayDeserializer import com.fasterxml.jackson.databind.module.SimpleModule +import loadTestCalendar import net.corda.finance.contracts.BusinessCalendar +import net.corda.finance.contracts.Expression import java.time.LocalDate +import java.util.* fun registerFinanceJSONMappers(objectMapper: ObjectMapper) { val financeModule = SimpleModule("finance").apply { addSerializer(BusinessCalendar::class.java, CalendarSerializer) addDeserializer(BusinessCalendar::class.java, CalendarDeserializer) + addSerializer(Expression::class.java, ExpressionSerializer) + addDeserializer(Expression::class.java, ExpressionDeserializer) } objectMapper.registerModule(financeModule) } -data class BusinessCalendarWrapper(val holidayDates: List) { +data class BusinessCalendarWrapper(val holidayDates: SortedSet) { fun toCalendar() = BusinessCalendar(holidayDates) } object CalendarSerializer : JsonSerializer() { override fun serialize(obj: BusinessCalendar, generator: JsonGenerator, context: SerializerProvider) { - val calendarName = BusinessCalendar.calendars.find { BusinessCalendar.getInstance(it) == obj } + val calendarName = TEST_CALENDAR_NAMES.find { loadTestCalendar(it) == obj } if (calendarName != null) { generator.writeString(calendarName) } else { @@ -38,8 +44,7 @@ object CalendarDeserializer : JsonDeserializer() { override fun deserialize(parser: JsonParser, context: DeserializationContext): BusinessCalendar { return try { try { - val array = StringArrayDeserializer.instance.deserialize(parser, context) - BusinessCalendar.getInstance(*array) + StringArrayDeserializer.instance.deserialize(parser, context).fold(BusinessCalendar.EMPTY) { acc, name -> acc + loadTestCalendar(name) } } catch (e: Exception) { parser.readValueAs(BusinessCalendarWrapper::class.java).toCalendar() } @@ -48,3 +53,11 @@ object CalendarDeserializer : JsonDeserializer() { } } } + +object ExpressionSerializer : JsonSerializer() { + override fun serialize(expr: Expression, generator: JsonGenerator, provider: SerializerProvider) = generator.writeString(expr.expr) +} + +object ExpressionDeserializer : JsonDeserializer() { + override fun deserialize(parser: JsonParser, context: DeserializationContext): Expression = Expression(parser.text) +} diff --git a/finance/workflows/src/main/kotlin/net/corda/finance/workflows/utils/FinanceWorkflowsUtils.kt b/finance/workflows/src/main/kotlin/net/corda/finance/workflows/utils/FinanceWorkflowsUtils.kt new file mode 100644 index 0000000000..c39097f6de --- /dev/null +++ b/finance/workflows/src/main/kotlin/net/corda/finance/workflows/utils/FinanceWorkflowsUtils.kt @@ -0,0 +1,15 @@ +import net.corda.core.flows.FlowException +import net.corda.core.serialization.CordaSerializable +import net.corda.finance.contracts.BusinessCalendar + +val TEST_CALENDAR_NAMES = listOf("London", "NewYork") + +fun loadTestCalendar(name: String): BusinessCalendar { + val stream = UnknownCalendar::class.java.getResourceAsStream("net/corda/finance/workflows/utils/${name}HolidayCalendar.txt") ?: throw UnknownCalendar(name) + return stream.use { + BusinessCalendar(stream.reader().readText().split(",").map { BusinessCalendar.parseDateFromString(it) }.toSortedSet()) + } +} + +@CordaSerializable +class UnknownCalendar(name: String) : FlowException("Calendar $name not found") diff --git a/finance/workflows/src/main/resources/META-INF/services/net.corda.finance.contracts.asset.AbstractCashSelection b/finance/workflows/src/main/resources/META-INF/services/net.corda.finance.contracts.asset.AbstractCashSelection deleted file mode 100644 index decfa4fadc..0000000000 --- a/finance/workflows/src/main/resources/META-INF/services/net.corda.finance.contracts.asset.AbstractCashSelection +++ /dev/null @@ -1,3 +0,0 @@ -net.corda.finance.contracts.asset.cash.selection.CashSelectionH2Impl -net.corda.finance.contracts.asset.cash.selection.CashSelectionPostgreSQLImpl -net.corda.finance.contracts.asset.cash.selection.CashSelectionSQLServerImpl diff --git a/finance/workflows/src/main/resources/META-INF/services/net.corda.finance.workflows.asset.selection.AbstractCashSelection b/finance/workflows/src/main/resources/META-INF/services/net.corda.finance.workflows.asset.selection.AbstractCashSelection new file mode 100644 index 0000000000..5eb04237d4 --- /dev/null +++ b/finance/workflows/src/main/resources/META-INF/services/net.corda.finance.workflows.asset.selection.AbstractCashSelection @@ -0,0 +1,3 @@ +net.corda.finance.workflows.asset.selection.CashSelectionH2Impl +net.corda.finance.workflows.asset.selection.CashSelectionPostgreSQLImpl +net.corda.finance.workflows.asset.selection.CashSelectionSQLServerImpl diff --git a/finance/contracts/src/main/resources/net/corda/finance/contracts/LondonHolidayCalendar.txt b/finance/workflows/src/main/resources/net/corda/finance/workflows/utils/LondonHolidayCalendar.txt similarity index 100% rename from finance/contracts/src/main/resources/net/corda/finance/contracts/LondonHolidayCalendar.txt rename to finance/workflows/src/main/resources/net/corda/finance/workflows/utils/LondonHolidayCalendar.txt diff --git a/finance/contracts/src/main/resources/net/corda/finance/contracts/NewYorkHolidayCalendar.txt b/finance/workflows/src/main/resources/net/corda/finance/workflows/utils/NewYorkHolidayCalendar.txt similarity index 100% rename from finance/contracts/src/main/resources/net/corda/finance/contracts/NewYorkHolidayCalendar.txt rename to finance/workflows/src/main/resources/net/corda/finance/workflows/utils/NewYorkHolidayCalendar.txt diff --git a/finance/workflows/src/test/kotlin/net/corda/finance/flows/CashSelectionTest.kt b/finance/workflows/src/test/kotlin/net/corda/finance/flows/CashSelectionTest.kt index 9aaa797b31..cd519e54c0 100644 --- a/finance/workflows/src/test/kotlin/net/corda/finance/flows/CashSelectionTest.kt +++ b/finance/workflows/src/test/kotlin/net/corda/finance/flows/CashSelectionTest.kt @@ -7,9 +7,9 @@ import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow import net.corda.finance.DOLLARS -import net.corda.finance.contracts.asset.AbstractCashSelection +import net.corda.finance.workflows.asset.selection.AbstractCashSelection import net.corda.finance.contracts.asset.Cash -import net.corda.finance.contracts.getCashBalance +import net.corda.finance.workflows.getCashBalance import net.corda.finance.issuedBy import net.corda.testing.core.singleIdentity import net.corda.testing.node.internal.FINANCE_CORDAPPS diff --git a/finance/workflows/src/test/kotlin/net/corda/finance/compat/CompatibilityTest.kt b/finance/workflows/src/test/kotlin/net/corda/finance/flows/CompatibilityTest.kt similarity index 98% rename from finance/workflows/src/test/kotlin/net/corda/finance/compat/CompatibilityTest.kt rename to finance/workflows/src/test/kotlin/net/corda/finance/flows/CompatibilityTest.kt index b329fd1a17..9bfc13fc17 100644 --- a/finance/workflows/src/test/kotlin/net/corda/finance/compat/CompatibilityTest.kt +++ b/finance/workflows/src/test/kotlin/net/corda/finance/flows/CompatibilityTest.kt @@ -1,4 +1,4 @@ -package net.corda.finance.compat +package net.corda.finance.flows import net.corda.core.serialization.SerializationDefaults import net.corda.core.serialization.SerializedBytes diff --git a/finance/workflows/src/test/kotlin/net/corda/finance/flows/test/CashExceptionThrowingFlow.kt b/finance/workflows/src/test/kotlin/net/corda/finance/flows/test/CashExceptionThrowingFlow.kt deleted file mode 100644 index a8e03cd67f..0000000000 --- a/finance/workflows/src/test/kotlin/net/corda/finance/flows/test/CashExceptionThrowingFlow.kt +++ /dev/null @@ -1,12 +0,0 @@ -package net.corda.finance.flows.test - -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.StartableByRPC -import net.corda.finance.flows.CashException - -@StartableByRPC -class CashExceptionThrowingFlow : FlowLogic() { - override fun call() { - throw CashException("BOOM!", IllegalStateException("Nope dude!")) - } -} diff --git a/finance/workflows/src/test/kotlin/net/corda/finance/contracts/asset/selection/CashSelectionH2ImplTest.kt b/finance/workflows/src/test/kotlin/net/corda/finance/workflows/asset/selection/CashSelectionH2ImplTest.kt similarity index 98% rename from finance/workflows/src/test/kotlin/net/corda/finance/contracts/asset/selection/CashSelectionH2ImplTest.kt rename to finance/workflows/src/test/kotlin/net/corda/finance/workflows/asset/selection/CashSelectionH2ImplTest.kt index fe1904d6d0..c231d3f7ac 100644 --- a/finance/workflows/src/test/kotlin/net/corda/finance/contracts/asset/selection/CashSelectionH2ImplTest.kt +++ b/finance/workflows/src/test/kotlin/net/corda/finance/workflows/asset/selection/CashSelectionH2ImplTest.kt @@ -1,4 +1,4 @@ -package net.corda.finance.contracts.asset.selection +package net.corda.finance.workflows.asset.selection import net.corda.core.internal.concurrent.transpose import net.corda.core.utilities.OpaqueBytes diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt index 88b6a569b4..fa4e752f6a 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt @@ -23,12 +23,12 @@ import net.corda.finance.DOLLARS import net.corda.finance.POUNDS import net.corda.finance.SWISS_FRANCS import net.corda.finance.contracts.asset.Cash -import net.corda.finance.contracts.asset.test.DummyFungibleContract +import net.corda.node.testing.DummyFungibleContract import net.corda.finance.schemas.CashSchemaV1 import net.corda.finance.test.SampleCashSchemaV1 import net.corda.finance.test.SampleCashSchemaV2 import net.corda.finance.test.SampleCashSchemaV3 -import net.corda.finance.utils.sumCash +import net.corda.finance.contracts.utils.sumCash import net.corda.node.services.api.IdentityServiceInternal import net.corda.node.services.api.WritableTransactionStorage import net.corda.node.services.schema.ContractStateAndRef @@ -42,7 +42,6 @@ import net.corda.nodeapi.internal.persistence.HibernateConfiguration import net.corda.nodeapi.internal.persistence.HibernateSchemaChangeException import net.corda.testing.core.* import net.corda.testing.internal.configureDatabase -import net.corda.testing.internal.rigorousMock import net.corda.testing.internal.vault.DummyDealStateSchemaV1 import net.corda.testing.internal.vault.DummyLinearStateSchemaV1 import net.corda.testing.internal.vault.DummyLinearStateSchemaV2 diff --git a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt index abfde97cdf..f1db35cabd 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt @@ -22,9 +22,10 @@ import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.toNonEmptySet import net.corda.finance.* import net.corda.finance.contracts.asset.Cash -import net.corda.finance.contracts.getCashBalance import net.corda.finance.schemas.CashSchemaV1 -import net.corda.finance.utils.sumCash +import net.corda.finance.contracts.utils.sumCash +import net.corda.finance.workflows.asset.CashUtils +import net.corda.finance.workflows.getCashBalance import net.corda.node.services.api.IdentityServiceInternal import net.corda.node.services.api.WritableTransactionStorage import net.corda.nodeapi.internal.persistence.CordaPersistence @@ -598,7 +599,7 @@ class NodeVaultServiceTest { database.transaction { val moveBuilder = TransactionBuilder(notary).apply { - Cash.generateSpend(services, this, Amount(1000, GBP), identity, thirdPartyIdentity) + CashUtils.generateSpend(services, this, Amount(1000, GBP), identity, thirdPartyIdentity) } val moveTx = moveBuilder.toWireTransaction(services) vaultService.notify(StatesToRecord.ONLY_RELEVANT, moveTx) @@ -657,7 +658,7 @@ class NodeVaultServiceTest { // Move cash val moveTxBuilder = database.transaction { TransactionBuilder(newNotary).apply { - Cash.generateSpend(services, this, Amount(amount.quantity, GBP), identity, thirdPartyIdentity.party.anonymise()) + CashUtils.generateSpend(services, this, Amount(amount.quantity, GBP), identity, thirdPartyIdentity.party.anonymise()) } } val moveTx = moveTxBuilder.toWireTransaction(services) diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt index 31976a9bff..2442d0594b 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -19,13 +19,14 @@ import net.corda.finance.* import net.corda.finance.contracts.CommercialPaper import net.corda.finance.contracts.Commodity import net.corda.finance.contracts.DealState +import net.corda.finance.workflows.asset.selection.AbstractCashSelection import net.corda.finance.contracts.asset.Cash -import net.corda.finance.contracts.asset.AbstractCashSelection import net.corda.finance.schemas.CashSchemaV1 import net.corda.finance.schemas.CashSchemaV1.PersistentCashState import net.corda.finance.schemas.CommercialPaperSchemaV1 import net.corda.finance.test.SampleCashSchemaV2 import net.corda.finance.test.SampleCashSchemaV3 +import net.corda.finance.workflows.CommercialPaperUtils import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.DatabaseTransaction @@ -33,7 +34,6 @@ import net.corda.testing.core.* import net.corda.testing.internal.TEST_TX_TIME import net.corda.testing.internal.chooseIdentity import net.corda.testing.internal.configureDatabase -import net.corda.testing.internal.rigorousMock import net.corda.testing.internal.vault.* import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices @@ -2118,7 +2118,7 @@ abstract class VaultQueryTestsBase : VaultQueryParties { // MegaCorp™ issues $10,000 of commercial paper, to mature in 30 days, owned by itself. val faceValue = 10000.DOLLARS `issued by` DUMMY_CASH_ISSUER val commercialPaper = - CommercialPaper().generateIssue(issuance, faceValue, TEST_TX_TIME + 30.days, DUMMY_NOTARY).let { builder -> + CommercialPaperUtils.generateIssue(issuance, faceValue, TEST_TX_TIME + 30.days, DUMMY_NOTARY).let { builder -> builder.setTimeWindow(TEST_TX_TIME, 30.seconds) val stx = services.signInitialTransaction(builder, MEGA_CORP_PUBKEY) notaryServices.addSignature(stx, DUMMY_NOTARY_KEY.public) @@ -2129,7 +2129,7 @@ abstract class VaultQueryTestsBase : VaultQueryParties { // MegaCorp™ now issues £10,000 of commercial paper, to mature in 30 days, owned by itself. val faceValue2 = 10000.POUNDS `issued by` DUMMY_CASH_ISSUER val commercialPaper2 = - CommercialPaper().generateIssue(issuance, faceValue2, TEST_TX_TIME + 30.days, DUMMY_NOTARY).let { builder -> + CommercialPaperUtils.generateIssue(issuance, faceValue2, TEST_TX_TIME + 30.days, DUMMY_NOTARY).let { builder -> builder.setTimeWindow(TEST_TX_TIME, 30.seconds) val stx = services.signInitialTransaction(builder, MEGA_CORP_PUBKEY) notaryServices.addSignature(stx, DUMMY_NOTARY_KEY.public) @@ -2155,7 +2155,7 @@ abstract class VaultQueryTestsBase : VaultQueryParties { // MegaCorp™ issues $10,000 of commercial paper, to mature in 30 days, owned by itself. val faceValue = 10000.DOLLARS `issued by` DUMMY_CASH_ISSUER val commercialPaper = - CommercialPaper().generateIssue(issuance, faceValue, TEST_TX_TIME + 30.days, DUMMY_NOTARY).let { builder -> + CommercialPaperUtils.generateIssue(issuance, faceValue, TEST_TX_TIME + 30.days, DUMMY_NOTARY).let { builder -> builder.setTimeWindow(TEST_TX_TIME, 30.seconds) val stx = services.signInitialTransaction(builder, MEGA_CORP_PUBKEY) notaryServices.addSignature(stx, DUMMY_NOTARY_KEY.public) @@ -2166,7 +2166,7 @@ abstract class VaultQueryTestsBase : VaultQueryParties { // MegaCorp™ now issues £5,000 of commercial paper, to mature in 30 days, owned by itself. val faceValue2 = 5000.POUNDS `issued by` DUMMY_CASH_ISSUER val commercialPaper2 = - CommercialPaper().generateIssue(issuance, faceValue2, TEST_TX_TIME + 30.days, DUMMY_NOTARY).let { builder -> + CommercialPaperUtils.generateIssue(issuance, faceValue2, TEST_TX_TIME + 30.days, DUMMY_NOTARY).let { builder -> builder.setTimeWindow(TEST_TX_TIME, 30.seconds) val stx = services.signInitialTransaction(builder, MEGA_CORP_PUBKEY) notaryServices.addSignature(stx, DUMMY_NOTARY_KEY.public) diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt index e81b570023..215780cea5 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt @@ -21,11 +21,12 @@ import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.getOrThrow import net.corda.finance.* import net.corda.finance.contracts.asset.Cash -import net.corda.finance.contracts.getCashBalance import net.corda.finance.schemas.CashSchemaV1 +import net.corda.finance.workflows.asset.CashUtils +import net.corda.finance.workflows.getCashBalance import net.corda.nodeapi.internal.persistence.CordaPersistence -import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.common.internal.addNotary +import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.* import net.corda.testing.internal.LogHelper import net.corda.testing.internal.vault.* @@ -137,7 +138,7 @@ class VaultWithCashTest { database.transaction { // A tx that spends our money. val spendTXBuilder = TransactionBuilder(DUMMY_NOTARY) - Cash.generateSpend(services, spendTXBuilder, 80.DOLLARS, services.myInfo.legalIdentitiesAndCerts.single(), BOB) + CashUtils.generateSpend(services, spendTXBuilder, 80.DOLLARS, services.myInfo.legalIdentitiesAndCerts.single(), BOB) val spendPTX = services.signInitialTransaction(spendTXBuilder, freshKey) notaryServices.addSignature(spendPTX) } @@ -185,7 +186,7 @@ class VaultWithCashTest { val first = backgroundExecutor.fork { database.transaction { val txn1Builder = TransactionBuilder(DUMMY_NOTARY) - Cash.generateSpend(services, txn1Builder, 60.DOLLARS, services.myInfo.legalIdentitiesAndCerts.single(), BOB) + CashUtils.generateSpend(services, txn1Builder, 60.DOLLARS, services.myInfo.legalIdentitiesAndCerts.single(), BOB) val ptxn1 = notaryServices.signInitialTransaction(txn1Builder) val txn1 = services.addSignature(ptxn1, freshKey) println("txn1: ${txn1.id} spent ${((txn1.tx.outputs[0].data) as Cash.State).amount}") @@ -216,7 +217,7 @@ class VaultWithCashTest { val second = backgroundExecutor.fork { database.transaction { val txn2Builder = TransactionBuilder(DUMMY_NOTARY) - Cash.generateSpend(services, txn2Builder, 80.DOLLARS, services.myInfo.legalIdentitiesAndCerts.single(), BOB) + CashUtils.generateSpend(services, txn2Builder, 80.DOLLARS, services.myInfo.legalIdentitiesAndCerts.single(), BOB) val ptxn2 = notaryServices.signInitialTransaction(txn2Builder) val txn2 = services.addSignature(ptxn2, freshKey) println("txn2: ${txn2.id} spent ${((txn2.tx.outputs[0].data) as Cash.State).amount}") @@ -341,7 +342,7 @@ class VaultWithCashTest { database.transaction { // A tx that spends our money. val spendTXBuilder = TransactionBuilder(DUMMY_NOTARY) - Cash.generateSpend(services, spendTXBuilder, 80.DOLLARS, services.myInfo.legalIdentitiesAndCerts.single(), BOB) + CashUtils.generateSpend(services, spendTXBuilder, 80.DOLLARS, services.myInfo.legalIdentitiesAndCerts.single(), BOB) val spendPTX = notaryServices.signInitialTransaction(spendTXBuilder) val spendTX = services.addSignature(spendPTX, freshKey) services.recordTransactions(spendTX) diff --git a/node/src/test/kotlin/net/corda/finance/contracts/asset/test/DummyFungibleContract.kt b/node/src/test/kotlin/net/corda/node/testing/DummyFungibleContract.kt similarity index 97% rename from node/src/test/kotlin/net/corda/finance/contracts/asset/test/DummyFungibleContract.kt rename to node/src/test/kotlin/net/corda/node/testing/DummyFungibleContract.kt index e4a993a47d..74aaaf36a1 100644 --- a/node/src/test/kotlin/net/corda/finance/contracts/asset/test/DummyFungibleContract.kt +++ b/node/src/test/kotlin/net/corda/node/testing/DummyFungibleContract.kt @@ -1,4 +1,4 @@ -package net.corda.finance.contracts.asset.test +package net.corda.node.testing import net.corda.core.contracts.* import net.corda.core.crypto.toStringShort @@ -12,9 +12,9 @@ import net.corda.finance.contracts.asset.OnLedgerAsset import net.corda.finance.test.SampleCashSchemaV1 import net.corda.finance.test.SampleCashSchemaV2 import net.corda.finance.test.SampleCashSchemaV3 -import net.corda.finance.utils.sumCash -import net.corda.finance.utils.sumCashOrNull -import net.corda.finance.utils.sumCashOrZero +import net.corda.finance.contracts.utils.sumCash +import net.corda.finance.contracts.utils.sumCashOrNull +import net.corda.finance.contracts.utils.sumCashOrZero import java.security.PublicKey import java.util.* diff --git a/samples/irs-demo/cordapp/contracts-irs/build.gradle b/samples/irs-demo/cordapp/contracts-irs/build.gradle index 270a90b29c..0d01b66393 100644 --- a/samples/irs-demo/cordapp/contracts-irs/build.gradle +++ b/samples/irs-demo/cordapp/contracts-irs/build.gradle @@ -6,10 +6,12 @@ dependencies { // The irs demo CorDapp depends upon Cash CorDapp features cordaCompile project(':core') cordaRuntime project(':node-api') + cordapp project(':finance:contracts') + + compile "com.fasterxml.jackson.core:jackson-annotations:${jackson_version}" testCompile project(':node-driver') testCompile "junit:junit:$junit_version" - cordapp project(':finance:contracts') } cordapp { diff --git a/samples/irs-demo/cordapp/contracts-irs/src/main/kotlin/net.corda.irs/contract/IRS.kt b/samples/irs-demo/cordapp/contracts-irs/src/main/kotlin/net/corda/irs/contract/IRS.kt similarity index 98% rename from samples/irs-demo/cordapp/contracts-irs/src/main/kotlin/net.corda.irs/contract/IRS.kt rename to samples/irs-demo/cordapp/contracts-irs/src/main/kotlin/net/corda/irs/contract/IRS.kt index fb999ba542..9bd4e1ec35 100644 --- a/samples/irs-demo/cordapp/contracts-irs/src/main/kotlin/net.corda.irs/contract/IRS.kt +++ b/samples/irs-demo/cordapp/contracts-irs/src/main/kotlin/net/corda/irs/contract/IRS.kt @@ -8,19 +8,7 @@ import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder -import net.corda.finance.contracts.AccrualAdjustment -import net.corda.finance.contracts.BusinessCalendar -import net.corda.finance.contracts.DateRollConvention -import net.corda.finance.contracts.DateRollDirection -import net.corda.finance.contracts.DayCountBasisDay -import net.corda.finance.contracts.DayCountBasisYear -import net.corda.finance.contracts.Expression -import net.corda.finance.contracts.Fix -import net.corda.finance.contracts.FixOf -import net.corda.finance.contracts.FixableDealState -import net.corda.finance.contracts.Frequency -import net.corda.finance.contracts.PaymentRule -import net.corda.finance.contracts.Tenor +import net.corda.finance.contracts.* import net.corda.irs.utilities.suggestInterestRateAnnouncementTimeWindow import org.apache.commons.jexl3.JexlBuilder import org.apache.commons.jexl3.MapContext diff --git a/samples/irs-demo/cordapp/contracts-irs/src/main/kotlin/net.corda.irs/contract/IRSExport.kt b/samples/irs-demo/cordapp/contracts-irs/src/main/kotlin/net/corda/irs/contract/IRSExport.kt similarity index 100% rename from samples/irs-demo/cordapp/contracts-irs/src/main/kotlin/net.corda.irs/contract/IRSExport.kt rename to samples/irs-demo/cordapp/contracts-irs/src/main/kotlin/net/corda/irs/contract/IRSExport.kt diff --git a/samples/irs-demo/cordapp/contracts-irs/src/main/kotlin/net.corda.irs/contract/IRSUtils.kt b/samples/irs-demo/cordapp/contracts-irs/src/main/kotlin/net/corda/irs/contract/IRSUtils.kt similarity index 100% rename from samples/irs-demo/cordapp/contracts-irs/src/main/kotlin/net.corda.irs/contract/IRSUtils.kt rename to samples/irs-demo/cordapp/contracts-irs/src/main/kotlin/net/corda/irs/contract/IRSUtils.kt diff --git a/samples/irs-demo/cordapp/contracts-irs/src/main/kotlin/net.corda.irs/utilities/OracleUtils.kt b/samples/irs-demo/cordapp/contracts-irs/src/main/kotlin/net/corda/irs/utilities/OracleUtils.kt similarity index 100% rename from samples/irs-demo/cordapp/contracts-irs/src/main/kotlin/net.corda.irs/utilities/OracleUtils.kt rename to samples/irs-demo/cordapp/contracts-irs/src/main/kotlin/net/corda/irs/utilities/OracleUtils.kt diff --git a/samples/irs-demo/cordapp/contracts-irs/src/test/kotlin/net.corda.irs/contract/IRSTests.kt b/samples/irs-demo/cordapp/contracts-irs/src/test/kotlin/net/corda/irs/contract/IRSTests.kt similarity index 98% rename from samples/irs-demo/cordapp/contracts-irs/src/test/kotlin/net.corda.irs/contract/IRSTests.kt rename to samples/irs-demo/cordapp/contracts-irs/src/test/kotlin/net/corda/irs/contract/IRSTests.kt index 93ee0483a6..e3cf3dd58e 100644 --- a/samples/irs-demo/cordapp/contracts-irs/src/test/kotlin/net.corda.irs/contract/IRSTests.kt +++ b/samples/irs-demo/cordapp/contracts-irs/src/test/kotlin/net/corda/irs/contract/IRSTests.kt @@ -3,6 +3,7 @@ package net.corda.irs.contract import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.mock import com.nhaarman.mockito_kotlin.whenever +import loadTestCalendar import net.corda.core.contracts.Amount import net.corda.core.contracts.UniqueIdentifier import net.corda.core.crypto.generateKeyPair @@ -15,8 +16,8 @@ import net.corda.finance.DOLLARS import net.corda.finance.EUR import net.corda.finance.contracts.* import net.corda.node.services.api.IdentityServiceInternal -import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.common.internal.addNotary +import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.DUMMY_NOTARY_NAME import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.TestIdentity @@ -61,7 +62,7 @@ fun createDummyIRS(irsSelect: Int): InterestRateSwap.State { dayInMonth = 10, paymentRule = PaymentRule.InArrears, paymentDelay = 3, - paymentCalendar = BusinessCalendar.getInstance("London", "NewYork"), + paymentCalendar = loadTestCalendar("London") + loadTestCalendar("NewYork"), interestPeriodAdjustment = AccrualAdjustment.Adjusted ) @@ -81,12 +82,12 @@ fun createDummyIRS(irsSelect: Int): InterestRateSwap.State { resetDayInMonth = 10, paymentRule = PaymentRule.InArrears, paymentDelay = 3, - paymentCalendar = BusinessCalendar.getInstance("London", "NewYork"), + paymentCalendar = loadTestCalendar("London") + loadTestCalendar("NewYork"), interestPeriodAdjustment = AccrualAdjustment.Adjusted, fixingPeriodOffset = 2, resetRule = PaymentRule.InAdvance, fixingsPerPayment = Frequency.Quarterly, - fixingCalendar = BusinessCalendar.getInstance("London"), + fixingCalendar = loadTestCalendar("London"), index = "LIBOR", indexSource = "TEL3750", indexTenor = Tenor("3M") @@ -122,7 +123,7 @@ fun createDummyIRS(irsSelect: Int): InterestRateSwap.State { interestRate = ReferenceRate("T3270", Tenor("6M"), "EONIA"), addressForTransfers = "", exposure = UnknownType(), - localBusinessDay = BusinessCalendar.getInstance("London"), + localBusinessDay = loadTestCalendar("London"), tradeID = "trade1", hashLegalDocs = "put hash here", dailyInterestAmount = Expression("(CashAmount * InterestRate ) / (fixedLeg.notional.currency.currencyCode.equals('GBP')) ? 365 : 360") @@ -149,7 +150,7 @@ fun createDummyIRS(irsSelect: Int): InterestRateSwap.State { dayInMonth = 10, paymentRule = PaymentRule.InArrears, paymentDelay = 0, - paymentCalendar = BusinessCalendar.getInstance(), + paymentCalendar = BusinessCalendar.EMPTY, interestPeriodAdjustment = AccrualAdjustment.Adjusted ) @@ -169,12 +170,12 @@ fun createDummyIRS(irsSelect: Int): InterestRateSwap.State { resetDayInMonth = 10, paymentRule = PaymentRule.InArrears, paymentDelay = 0, - paymentCalendar = BusinessCalendar.getInstance(), + paymentCalendar = BusinessCalendar.EMPTY, interestPeriodAdjustment = AccrualAdjustment.Adjusted, fixingPeriodOffset = 2, resetRule = PaymentRule.InAdvance, fixingsPerPayment = Frequency.Quarterly, - fixingCalendar = BusinessCalendar.getInstance(), + fixingCalendar = BusinessCalendar.EMPTY, index = "USD LIBOR", indexSource = "TEL3750", indexTenor = Tenor("3M") @@ -210,7 +211,7 @@ fun createDummyIRS(irsSelect: Int): InterestRateSwap.State { interestRate = ReferenceRate("T3270", Tenor("6M"), "EONIA"), addressForTransfers = "", exposure = UnknownType(), - localBusinessDay = BusinessCalendar.getInstance("London"), + localBusinessDay = loadTestCalendar("London"), tradeID = "trade2", hashLegalDocs = "put hash here", dailyInterestAmount = Expression("(CashAmount * InterestRate ) / (fixedLeg.notional.currency.currencyCode.equals('GBP')) ? 365 : 360") diff --git a/samples/irs-demo/cordapp/workflows-irs/src/main/kotlin/net.corda.irs/api/NodeInterestRates.kt b/samples/irs-demo/cordapp/workflows-irs/src/main/kotlin/net.corda.irs/api/NodeInterestRates.kt index 1f4a23d437..b1a7e500b3 100644 --- a/samples/irs-demo/cordapp/workflows-irs/src/main/kotlin/net.corda.irs/api/NodeInterestRates.kt +++ b/samples/irs-demo/cordapp/workflows-irs/src/main/kotlin/net.corda.irs/api/NodeInterestRates.kt @@ -1,6 +1,7 @@ package net.corda.irs.api import co.paralleluniverse.fibers.Suspendable +import loadTestCalendar import net.corda.core.contracts.Command import net.corda.core.crypto.TransactionSignature import net.corda.core.flows.* @@ -15,10 +16,10 @@ import net.corda.finance.contracts.BusinessCalendar import net.corda.finance.contracts.Fix import net.corda.finance.contracts.FixOf import net.corda.finance.contracts.Tenor -import net.corda.finance.contracts.math.CubicSplineInterpolator -import net.corda.finance.contracts.math.Interpolator -import net.corda.finance.contracts.math.InterpolatorFactory import net.corda.irs.flows.RatesFixFlow +import net.corda.irs.math.CubicSplineInterpolator +import net.corda.irs.math.Interpolator +import net.corda.irs.math.InterpolatorFactory import org.apache.commons.io.IOUtils import java.math.BigDecimal import java.time.LocalDate @@ -193,7 +194,7 @@ object NodeInterestRates { } // TODO: the calendar data needs to be specified for every fix type in the input string - val calendar = BusinessCalendar.getInstance("London", "NewYork") + val calendar = loadTestCalendar("London") + loadTestCalendar("NewYork") return tempContainer.mapValues { InterpolatingRateMap(it.key.second, it.value, calendar, factory) } } diff --git a/finance/contracts/src/main/kotlin/net/corda/finance/contracts/math/Interpolators.kt b/samples/irs-demo/cordapp/workflows-irs/src/main/kotlin/net/corda/irs/math/Interpolators.kt similarity index 99% rename from finance/contracts/src/main/kotlin/net/corda/finance/contracts/math/Interpolators.kt rename to samples/irs-demo/cordapp/workflows-irs/src/main/kotlin/net/corda/irs/math/Interpolators.kt index de2eb3bea2..701210411d 100644 --- a/finance/contracts/src/main/kotlin/net/corda/finance/contracts/math/Interpolators.kt +++ b/samples/irs-demo/cordapp/workflows-irs/src/main/kotlin/net/corda/irs/math/Interpolators.kt @@ -1,4 +1,4 @@ -package net.corda.finance.contracts.math +package net.corda.irs.math import java.util.* diff --git a/samples/irs-demo/cordapp/workflows-irs/src/test/kotlin/net.corda.irs/Main.kt b/samples/irs-demo/cordapp/workflows-irs/src/test/kotlin/net/corda/irs/Main.kt similarity index 100% rename from samples/irs-demo/cordapp/workflows-irs/src/test/kotlin/net.corda.irs/Main.kt rename to samples/irs-demo/cordapp/workflows-irs/src/test/kotlin/net/corda/irs/Main.kt diff --git a/samples/irs-demo/cordapp/workflows-irs/src/test/kotlin/net.corda.irs/api/NodeInterestRatesTest.kt b/samples/irs-demo/cordapp/workflows-irs/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt similarity index 100% rename from samples/irs-demo/cordapp/workflows-irs/src/test/kotlin/net.corda.irs/api/NodeInterestRatesTest.kt rename to samples/irs-demo/cordapp/workflows-irs/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt diff --git a/samples/irs-demo/cordapp/workflows-irs/src/test/kotlin/net.corda.irs/api/OracleNodeTearOffTests.kt b/samples/irs-demo/cordapp/workflows-irs/src/test/kotlin/net/corda/irs/api/OracleNodeTearOffTests.kt similarity index 100% rename from samples/irs-demo/cordapp/workflows-irs/src/test/kotlin/net.corda.irs/api/OracleNodeTearOffTests.kt rename to samples/irs-demo/cordapp/workflows-irs/src/test/kotlin/net/corda/irs/api/OracleNodeTearOffTests.kt diff --git a/samples/irs-demo/cordapp/workflows-irs/src/test/kotlin/net.corda.irs/flows/UpdateBusinessDayFlow.kt b/samples/irs-demo/cordapp/workflows-irs/src/test/kotlin/net/corda/irs/flows/UpdateBusinessDayFlow.kt similarity index 100% rename from samples/irs-demo/cordapp/workflows-irs/src/test/kotlin/net.corda.irs/flows/UpdateBusinessDayFlow.kt rename to samples/irs-demo/cordapp/workflows-irs/src/test/kotlin/net/corda/irs/flows/UpdateBusinessDayFlow.kt diff --git a/finance/contracts/src/test/kotlin/net/corda/finance/contracts/math/InterpolatorsTest.kt b/samples/irs-demo/cordapp/workflows-irs/src/test/kotlin/net/corda/irs/math/InterpolatorsTest.kt similarity index 98% rename from finance/contracts/src/test/kotlin/net/corda/finance/contracts/math/InterpolatorsTest.kt rename to samples/irs-demo/cordapp/workflows-irs/src/test/kotlin/net/corda/irs/math/InterpolatorsTest.kt index 4534663689..d004970841 100644 --- a/finance/contracts/src/test/kotlin/net/corda/finance/contracts/math/InterpolatorsTest.kt +++ b/samples/irs-demo/cordapp/workflows-irs/src/test/kotlin/net/corda/irs/math/InterpolatorsTest.kt @@ -1,4 +1,4 @@ -package net.corda.finance.contracts.math +package net.corda.irs.math import org.junit.Assert import org.junit.Test diff --git a/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt b/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt index cce8b0b41a..32dd408ef6 100644 --- a/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt +++ b/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt @@ -19,7 +19,7 @@ import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.seconds -import net.corda.finance.plugin.registerFinanceJSONMappers +import net.corda.finance.workflows.plugin.registerFinanceJSONMappers import net.corda.irs.contract.InterestRateSwap import net.corda.irs.web.IrsDemoWebApplication import net.corda.test.spring.springDriver @@ -31,6 +31,8 @@ import net.corda.testing.driver.DriverParameters import net.corda.testing.http.HttpApi import net.corda.testing.node.NotarySpec import net.corda.testing.node.User +import net.corda.testing.node.internal.FINANCE_CORDAPPS +import net.corda.testing.node.internal.cordappWithPackages import org.apache.commons.io.IOUtils import org.assertj.core.api.Assertions.assertThat import org.junit.Test @@ -53,13 +55,13 @@ class IRSDemoTest { springDriver(DriverParameters( useTestClock = true, notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, rpcUsers = rpcUsers)), - extraCordappPackagesToScan = listOf("net.corda.irs", "net.corda.finance", "migration") + cordappsForAllNodes = FINANCE_CORDAPPS + cordappWithPackages("net.corda.irs") )) { - val (notary, nodeA, nodeB, controller) = listOf( - defaultNotaryNode, + val (nodeA, nodeB, controller) = listOf( startNode(providedName = DUMMY_BANK_A_NAME, rpcUsers = rpcUsers), startNode(providedName = DUMMY_BANK_B_NAME, rpcUsers = rpcUsers), - startNode(providedName = CordaX500Name("Regulator", "Moscow", "RU")) + startNode(providedName = CordaX500Name("Regulator", "Moscow", "RU")), + defaultNotaryNode ).map { it.getOrThrow() } log.info("All nodes started") diff --git a/samples/irs-demo/web/src/main/kotlin/net/corda/irs/web/IrsDemoWebApplication.kt b/samples/irs-demo/web/src/main/kotlin/net/corda/irs/web/IrsDemoWebApplication.kt index 394f393ff2..9e93b0bbe6 100644 --- a/samples/irs-demo/web/src/main/kotlin/net/corda/irs/web/IrsDemoWebApplication.kt +++ b/samples/irs-demo/web/src/main/kotlin/net/corda/irs/web/IrsDemoWebApplication.kt @@ -6,7 +6,7 @@ import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.RPCException import net.corda.core.messaging.CordaRPCOps import net.corda.core.utilities.NetworkHostAndPort -import net.corda.finance.plugin.registerFinanceJSONMappers +import net.corda.finance.workflows.plugin.registerFinanceJSONMappers import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Value diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/webplugin/SimmPlugin.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/webplugin/SimmPlugin.kt index 24aeda987b..9c235b303b 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/webplugin/SimmPlugin.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/webplugin/SimmPlugin.kt @@ -2,7 +2,7 @@ package net.corda.vega.webplugin import com.fasterxml.jackson.databind.ObjectMapper import net.corda.core.serialization.SerializationWhitelist -import net.corda.finance.plugin.registerFinanceJSONMappers +import net.corda.finance.workflows.plugin.registerFinanceJSONMappers import net.corda.vega.api.PortfolioApi import net.corda.webserver.services.WebServerPluginRegistry import java.util.function.Function diff --git a/samples/trader-demo/workflows-trader/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt b/samples/trader-demo/workflows-trader/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt index cb82d46472..cad856bb73 100644 --- a/samples/trader-demo/workflows-trader/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt +++ b/samples/trader-demo/workflows-trader/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt @@ -1,14 +1,12 @@ package net.corda.traderdemo import net.corda.client.rpc.CordaRPCClient -import net.corda.core.identity.CordaX500Name -import net.corda.core.internal.div import net.corda.core.messaging.startFlow import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.millis import net.corda.finance.DOLLARS import net.corda.finance.USD -import net.corda.finance.contracts.getCashBalance +import net.corda.finance.workflows.getCashBalance import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow import net.corda.node.services.Permissions.Companion.all @@ -27,7 +25,6 @@ import net.corda.traderdemo.flow.CommercialPaperIssueFlow import net.corda.traderdemo.flow.SellerFlow import org.assertj.core.api.Assertions.assertThat import org.junit.Test -import java.sql.DriverManager import java.util.concurrent.Executors class TraderDemoTest { diff --git a/samples/trader-demo/workflows-trader/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt b/samples/trader-demo/workflows-trader/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt index 7c2a65c031..3d219bb944 100644 --- a/samples/trader-demo/workflows-trader/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt +++ b/samples/trader-demo/workflows-trader/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt @@ -14,7 +14,7 @@ import net.corda.finance.DOLLARS import net.corda.finance.USD import net.corda.finance.contracts.CommercialPaper import net.corda.finance.contracts.asset.Cash -import net.corda.finance.contracts.getCashBalance +import net.corda.finance.workflows.getCashBalance import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow import net.corda.node.services.vault.VaultSchemaV1 diff --git a/samples/trader-demo/workflows-trader/src/main/kotlin/net/corda/traderdemo/flow/CommercialPaperIssueFlow.kt b/samples/trader-demo/workflows-trader/src/main/kotlin/net/corda/traderdemo/flow/CommercialPaperIssueFlow.kt index e8a461bf50..fa46802a38 100644 --- a/samples/trader-demo/workflows-trader/src/main/kotlin/net/corda/traderdemo/flow/CommercialPaperIssueFlow.kt +++ b/samples/trader-demo/workflows-trader/src/main/kotlin/net/corda/traderdemo/flow/CommercialPaperIssueFlow.kt @@ -12,7 +12,7 @@ import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.days import net.corda.core.utilities.seconds import net.corda.finance.`issued by` -import net.corda.finance.contracts.CommercialPaper +import net.corda.finance.workflows.CommercialPaperUtils import java.time.Instant import java.util.* @@ -41,7 +41,7 @@ class CommercialPaperIssueFlow(private val amount: Amount, progressTracker.currentStep = ISSUING val issuance: SignedTransaction = run { - val tx = CommercialPaper().generateIssue(ourIdentity.ref(issueRef), amount `issued by` ourIdentity.ref(issueRef), + val tx = CommercialPaperUtils.generateIssue(ourIdentity.ref(issueRef), amount `issued by` ourIdentity.ref(issueRef), Instant.now() + 10.days, notary) // TODO: Consider moving these two steps below into generateIssue. @@ -62,7 +62,7 @@ class CommercialPaperIssueFlow(private val amount: Amount, return run { val builder = TransactionBuilder(notary) - CommercialPaper().generateMove(builder, issuance.tx.outRef(0), recipient) + CommercialPaperUtils.generateMove(builder, issuance.tx.outRef(0), recipient) val stx = serviceHub.signInitialTransaction(builder) val recipientSession = initiateFlow(recipient) subFlow(FinalityFlow(stx, listOf(recipientSession))) diff --git a/samples/trader-demo/workflows-trader/src/main/kotlin/net/corda/traderdemo/flow/LoggingBuyerFlow.kt b/samples/trader-demo/workflows-trader/src/main/kotlin/net/corda/traderdemo/flow/LoggingBuyerFlow.kt index c8bfdd9a4d..c32c3fe664 100644 --- a/samples/trader-demo/workflows-trader/src/main/kotlin/net/corda/traderdemo/flow/LoggingBuyerFlow.kt +++ b/samples/trader-demo/workflows-trader/src/main/kotlin/net/corda/traderdemo/flow/LoggingBuyerFlow.kt @@ -6,7 +6,7 @@ import net.corda.core.flows.InitiatedBy import net.corda.core.internal.Emoji import net.corda.core.transactions.SignedTransaction import net.corda.finance.contracts.CommercialPaper -import net.corda.finance.contracts.getCashBalances +import net.corda.finance.workflows.getCashBalances import net.corda.traderdemo.TransactionGraphSearch @InitiatedBy(SellerFlow::class) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalTestUtils.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalTestUtils.kt index 3a14edf1b3..de3b239c54 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalTestUtils.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalTestUtils.kt @@ -55,9 +55,8 @@ private val log = LoggerFactory.getLogger("net.corda.testing.internal.InternalTe * * You will probably need to use [FINANCE_CORDAPPS] instead to get access to the flows as well. */ -// TODO We can't use net.corda.finance.contracts as finance-workflows contains the package net.corda.finance.contracts.asset.cash.selection. This should be renamed. @JvmField -val FINANCE_CONTRACTS_CORDAPP: TestCordappImpl = findCordapp("net.corda.finance.schemas") +val FINANCE_CONTRACTS_CORDAPP: TestCordappImpl = findCordapp("net.corda.finance.contracts") /** * Reference to the finance-workflows CorDapp in this repo. The metadata is taken directly from finance/workflows/build.gradle, including the @@ -66,7 +65,7 @@ val FINANCE_CONTRACTS_CORDAPP: TestCordappImpl = findCordapp("net.corda.finance. * You will probably need to use [FINANCE_CORDAPPS] instead to get access to the contract classes as well. */ @JvmField -val FINANCE_WORKFLOWS_CORDAPP: TestCordappImpl = findCordapp("net.corda.finance.flows") +val FINANCE_WORKFLOWS_CORDAPP: TestCordappImpl = findCordapp("net.corda.finance.workflows") @JvmField val FINANCE_CORDAPPS: Set = setOf(FINANCE_CONTRACTS_CORDAPP, FINANCE_WORKFLOWS_CORDAPP) diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt index 485ee55e03..c4edd1c0fc 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt @@ -18,9 +18,13 @@ import net.corda.finance.contracts.DealState import net.corda.finance.contracts.asset.Cash import net.corda.finance.contracts.asset.Obligation import net.corda.finance.contracts.asset.OnLedgerAsset +import net.corda.finance.workflows.asset.CashUtils import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyState -import net.corda.testing.core.* +import net.corda.testing.core.DummyCommandData +import net.corda.testing.core.TestIdentity +import net.corda.testing.core.dummyCommand +import net.corda.testing.core.singleIdentity import net.corda.testing.internal.chooseIdentity import net.corda.testing.internal.chooseIdentityAndCert import java.security.PublicKey @@ -308,7 +312,7 @@ class VaultFiller @JvmOverloads constructor( val update = services.vaultService.rawUpdates.toFuture() // A tx that spends our money. val builder = TransactionBuilder(altNotary).apply { - Cash.generateSpend(services, this, amount, ourIdentity, to) + CashUtils.generateSpend(services, this, amount, ourIdentity, to) } val spendTx = services.signInitialTransaction(builder, altNotary.owningKey) services.recordTransactions(spendTx) diff --git a/tools/demobench/build.gradle b/tools/demobench/build.gradle index 9d07bb53ee..74e7df1ddb 100644 --- a/tools/demobench/build.gradle +++ b/tools/demobench/build.gradle @@ -43,16 +43,17 @@ configurations { } dependencies { + compile project(':tools:explorer') + compile project(':client:rpc') + compile project(':finance:contracts') + compile project(':finance:workflows') + // TornadoFX: A lightweight Kotlin framework for working with JavaFX UI's. compile "no.tornado:tornadofx:$tornadofx_version" // Controls FX: more java FX components http://fxexperience.com/controlsfx/ compile "org.controlsfx:controlsfx:$controlsfx_version" - compile project(':client:rpc') - compile project(':finance:contracts') - compile project(':finance:workflows') - compile "com.h2database:h2:$h2_version" compile "net.java.dev.jna:jna-platform:$jna_version" compile "com.google.guava:guava:$guava_version" diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeData.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeData.kt index 207d612790..40b35ca9bf 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeData.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeData.kt @@ -5,7 +5,7 @@ import javafx.beans.property.SimpleListProperty import javafx.beans.property.SimpleObjectProperty import javafx.beans.property.SimpleStringProperty import javafx.collections.FXCollections.observableArrayList -import net.corda.finance.utils.CityDatabase +import net.corda.explorer.CityDatabase import tornadofx.* import java.util.* diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt index 3d70da296c..e4c012c3aa 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt @@ -18,12 +18,12 @@ import javafx.util.StringConverter import net.corda.core.internal.* import net.corda.demobench.model.* import net.corda.demobench.ui.CloseableTab +import net.corda.explorer.CityDatabase +import net.corda.explorer.WorldMapLocation import net.corda.finance.CHF import net.corda.finance.EUR import net.corda.finance.GBP import net.corda.finance.USD -import net.corda.finance.utils.CityDatabase -import net.corda.finance.utils.WorldMapLocation import org.controlsfx.control.CheckListView import tornadofx.* import java.nio.file.Path diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTerminalView.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTerminalView.kt index a63943bcc6..4bb92b7dc1 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTerminalView.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTerminalView.kt @@ -29,7 +29,7 @@ import net.corda.demobench.rpc.NodeRPC import net.corda.demobench.ui.PropertyLabel import net.corda.demobench.web.DBViewer import net.corda.demobench.web.WebServerController -import net.corda.finance.contracts.getCashBalances +import net.corda.finance.workflows.getCashBalances import rx.Subscription import rx.schedulers.Schedulers import tornadofx.* diff --git a/finance/contracts/src/main/kotlin/net/corda/finance/utils/PhysicalLocationStructures.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/PhysicalLocationStructures.kt similarity index 99% rename from finance/contracts/src/main/kotlin/net/corda/finance/utils/PhysicalLocationStructures.kt rename to tools/explorer/src/main/kotlin/net/corda/explorer/PhysicalLocationStructures.kt index 0e54a2ffba..18eb966c53 100644 --- a/finance/contracts/src/main/kotlin/net/corda/finance/utils/PhysicalLocationStructures.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/PhysicalLocationStructures.kt @@ -1,4 +1,4 @@ -package net.corda.finance.utils +package net.corda.explorer import net.corda.core.serialization.CordaSerializable import java.util.* diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt index a76e051f68..647218c625 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt @@ -33,11 +33,11 @@ import net.corda.core.identity.Party import net.corda.core.node.NodeInfo import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.toBase58String +import net.corda.explorer.CityDatabase +import net.corda.explorer.ScreenCoordinate +import net.corda.explorer.WorldMapLocation import net.corda.explorer.formatters.PartyNameFormatter import net.corda.explorer.model.CordaView -import net.corda.finance.utils.CityDatabase -import net.corda.finance.utils.ScreenCoordinate -import net.corda.finance.utils.WorldMapLocation import tornadofx.* class Network : CordaView() { diff --git a/finance/contracts/src/main/resources/net/corda/finance/utils/cities.txt b/tools/explorer/src/main/resources/net/corda/explorer/cities.txt similarity index 100% rename from finance/contracts/src/main/resources/net/corda/finance/utils/cities.txt rename to tools/explorer/src/main/resources/net/corda/explorer/cities.txt diff --git a/finance/contracts/src/test/kotlin/net/corda/finance/utils/CityDatabaseTest.kt b/tools/explorer/src/test/kotlin/net/corda/explorer/CityDatabaseTest.kt similarity index 91% rename from finance/contracts/src/test/kotlin/net/corda/finance/utils/CityDatabaseTest.kt rename to tools/explorer/src/test/kotlin/net/corda/explorer/CityDatabaseTest.kt index e0e5036146..988a13bf68 100644 --- a/finance/contracts/src/test/kotlin/net/corda/finance/utils/CityDatabaseTest.kt +++ b/tools/explorer/src/test/kotlin/net/corda/explorer/CityDatabaseTest.kt @@ -1,4 +1,4 @@ -package net.corda.finance.utils +package net.corda.explorer import org.junit.Assert.assertEquals import org.junit.Test