mirror of
https://github.com/corda/corda.git
synced 2024-12-19 21:17:58 +00:00
CORDA-2467: Moved non-contract things out of finance-contracts and into finance-workflows (#4700)
This includes all of the cash selection logic, JSON support using jackson and a bunch of utilities which are not relevant to contract verification. The exception to this are Interpolator which has been moved to the IRS demo, and PhysicalLocationStructures.kt which is now in explorer.
This commit is contained in:
parent
deb66d1396
commit
c39c61ecab
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
||||
|
@ -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
|
||||
|
@ -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.*
|
||||
|
@ -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 <https://github.com/corda/corda/tree/master/samples/irs-demo/cordapp/workflows-irs>`_.
|
||||
|
||||
.. _changelog_v4.0:
|
||||
|
||||
Version 4.0
|
||||
|
@ -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
|
||||
|
@ -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<State>, 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(),
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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<Issued<Currency>> 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<Issued<Currency>> faceValue, @Nullable Instant maturityDate, @NotNull Party notary) {
|
||||
return generateIssue(issuance, faceValue, maturityDate, notary, null);
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
public void generateRedeem(final TransactionBuilder tx,
|
||||
final StateAndRef<State> 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<State> 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> T onlyElementOf(Iterable<T> iterable) {
|
||||
Iterator<T> iter = iterable.iterator();
|
||||
T item = iter.next();
|
||||
|
@ -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<LocalDate>) {
|
||||
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<LocalDate> {
|
||||
val ret = ArrayList<LocalDate>()
|
||||
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)"
|
||||
}
|
@ -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<Issued<Currency>>, 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<State>, 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<State>, 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)
|
||||
}
|
||||
}
|
@ -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<Expression>() {
|
||||
override fun serialize(expr: Expression, generator: JsonGenerator, provider: SerializerProvider) {
|
||||
generator.writeString(expr.expr)
|
||||
}
|
||||
}
|
||||
|
||||
object ExpressionDeserializer : JsonDeserializer<Expression>() {
|
||||
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<LocalDate>) {
|
||||
@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<LocalDate> {
|
||||
val ret = ArrayList<LocalDate>()
|
||||
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. */
|
||||
|
@ -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<Currency, Cash.Commands, Cash.State>() {
|
||||
|
||||
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<Currency>,
|
||||
to: AbstractParty,
|
||||
onlyFromParties: Set<AbstractParty> = 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<Currency>,
|
||||
ourIdentity: PartyAndCertificate,
|
||||
to: AbstractParty,
|
||||
onlyFromParties: Set<AbstractParty> = emptySet()): Pair<TransactionBuilder, List<PublicKey>> {
|
||||
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<PartyAndAmount<Currency>>,
|
||||
onlyFromParties: Set<AbstractParty> = 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<PartyAndAmount<Currency>>,
|
||||
ourIdentity: PartyAndCertificate,
|
||||
onlyFromParties: Set<AbstractParty> = emptySet()): Pair<TransactionBuilder, List<PublicKey>> {
|
||||
fun deriveState(txState: TransactionState<Cash.State>, amt: Amount<Issued<Currency>>, 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() })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<P : Any> : 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<State<P>>) {
|
||||
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<P>::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<Issued<Terms<P>>>,
|
||||
assetStates: List<StateAndRef<Obligation.State<P>>>): Set<PublicKey> {
|
||||
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<Issued<Currency>>,
|
||||
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<P>,
|
||||
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<Obligation.Terms<P>>,
|
||||
notary: Party,
|
||||
vararg inputs: StateAndRef<State<P>>) {
|
||||
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<PublicKey, AbstractParty>()
|
||||
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<StateAndRef<State<P>>>,
|
||||
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<AbstractParty>()
|
||||
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<StateAndRef<State<P>>>,
|
||||
assetStatesAndRefs: Iterable<StateAndRef<FungibleAsset<P>>>,
|
||||
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<P> = issuanceDef.product
|
||||
val obligationTotal: Amount<P> = Amount(states.map { it.data }.sumObligations<P>().quantity, template.product)
|
||||
var obligationRemaining: Amount<P> = obligationTotal
|
||||
val assetSigners = HashSet<AbstractParty>()
|
||||
|
||||
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<State<P>>): Issued<Terms<P>> =
|
||||
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<State<P>>) =
|
||||
states.map { it.template }.distinct().single()
|
||||
}
|
||||
|
||||
|
||||
|
@ -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
|
@ -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<SignedTransaction, UUID> {
|
||||
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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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<Currency>().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<Currency>().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<Obligation.State<Currency>>(0))
|
||||
Obligation<Currency>().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<Currency>().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<Currency>().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<Currency>().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<Currency>().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<Currency>().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<Currency>().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<Currency>().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<Currency>().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<Currency>().generateSettle(this, listOf(obligationTx.outRef(0)), listOf(cashTx.outRef(0)), Cash.Commands.Move(), DUMMY_NOTARY)
|
||||
ObligationUtils.generateSettle(this, listOf(obligationTx.outRef<Obligation.State<Currency>>(0)), listOf(cashTx.outRef(0)), Cash.Commands.Move(), DUMMY_NOTARY)
|
||||
}.toWireTransaction(miniCorpServices)
|
||||
assertEquals(2, settleTx.inputs.size)
|
||||
assertEquals(1, settleTx.outputs.size)
|
||||
|
@ -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')
|
||||
|
@ -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<Unit>() {
|
||||
override fun call(): Unit = throw CashException("BOOM!", IllegalStateException("Nope dude!"))
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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<Party> = emptySet(),
|
||||
val notary: Party? = null) : AbstractCashFlow<AbstractCashFlow.Result>(progressTracker) {
|
||||
val notary: Party? = null
|
||||
) : AbstractCashFlow<AbstractCashFlow.Result>(progressTracker) {
|
||||
/** A straightforward constructor that constructs spends using cash states of any issuer. */
|
||||
constructor(amount: Amount<Currency>, 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,
|
||||
|
@ -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)
|
||||
|
@ -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<Issued<Currency>>, 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<CommercialPaper.State>, 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<CommercialPaper.State>, 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)
|
||||
}
|
||||
}
|
@ -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
|
@ -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<Currency>,
|
||||
to: AbstractParty,
|
||||
onlyFromParties: Set<AbstractParty> = emptySet()): Pair<TransactionBuilder, List<PublicKey>> {
|
||||
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<Currency>,
|
||||
ourIdentity: PartyAndCertificate,
|
||||
to: AbstractParty,
|
||||
onlyFromParties: Set<AbstractParty> = emptySet()): Pair<TransactionBuilder, List<PublicKey>> {
|
||||
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<PartyAndAmount<Currency>>,
|
||||
onlyFromParties: Set<AbstractParty> = emptySet()): Pair<TransactionBuilder, List<PublicKey>> {
|
||||
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<PartyAndAmount<Currency>>,
|
||||
ourIdentity: PartyAndCertificate,
|
||||
onlyFromParties: Set<AbstractParty> = emptySet()): Pair<TransactionBuilder, List<PublicKey>> {
|
||||
fun deriveState(txState: TransactionState<Cash.State>, amt: Amount<Issued<Currency>>, owner: AbstractParty): TransactionState<Cash.State> {
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
@ -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<Issued<Currency>>,
|
||||
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 <P : Any> generateIssue(tx: TransactionBuilder,
|
||||
obligor: AbstractParty,
|
||||
issuanceDef: Obligation.Terms<P>,
|
||||
pennies: Long,
|
||||
beneficiary: AbstractParty,
|
||||
notary: Party): Set<PublicKey> {
|
||||
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 <P : Any> generateCloseOutNetting(tx: TransactionBuilder, signer: AbstractParty, vararg inputs: StateAndRef<Obligation.State<P>>) {
|
||||
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<P>::net)
|
||||
if (out.quantity > 0L) {
|
||||
tx.addOutputState(out, Obligation.PROGRAM_ID)
|
||||
}
|
||||
tx.addCommand(Obligation.Commands.Net(NetType.PAYMENT), signer.owningKey)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun <P : Any> generatePaymentNetting(tx: TransactionBuilder,
|
||||
issued: Issued<Obligation.Terms<P>>,
|
||||
notary: Party,
|
||||
vararg inputs: StateAndRef<Obligation.State<P>>) {
|
||||
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<PublicKey, AbstractParty>()
|
||||
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 <P : Any> generateSetLifecycle(tx: TransactionBuilder,
|
||||
statesAndRefs: List<StateAndRef<Obligation.State<P>>>,
|
||||
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<AbstractParty>()
|
||||
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 <P : Any> getTermsOrThrow(states: Iterable<Obligation.State<P>>) = 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 <P : Any> generateSettle(tx: TransactionBuilder,
|
||||
statesAndRefs: Iterable<StateAndRef<Obligation.State<P>>>,
|
||||
assetStatesAndRefs: Iterable<StateAndRef<FungibleAsset<P>>>,
|
||||
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<P> = issuanceDef.product
|
||||
val obligationTotal: Amount<P> = Amount(states.map { it.data }.sumObligations<P>().quantity, template.product)
|
||||
var obligationRemaining: Amount<P> = obligationTotal
|
||||
val assetSigners = HashSet<AbstractParty>()
|
||||
|
||||
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 <P : Any> getIssuanceDefinitionOrThrow(states: Iterable<Obligation.State<P>>): Issued<Obligation.Terms<P>> {
|
||||
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 <P : Any> generateExit(tx: TransactionBuilder,
|
||||
amountIssued: Amount<Issued<Obligation.Terms<P>>>,
|
||||
assetStates: List<StateAndRef<Obligation.State<P>>>): Set<PublicKey> {
|
||||
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) }
|
||||
)
|
||||
}
|
||||
}
|
@ -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,
|
@ -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
|
@ -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
|
@ -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
|
@ -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<LocalDate>) {
|
||||
data class BusinessCalendarWrapper(val holidayDates: SortedSet<LocalDate>) {
|
||||
fun toCalendar() = BusinessCalendar(holidayDates)
|
||||
}
|
||||
|
||||
object CalendarSerializer : JsonSerializer<BusinessCalendar>() {
|
||||
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<BusinessCalendar>() {
|
||||
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<BusinessCalendar>() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object ExpressionSerializer : JsonSerializer<Expression>() {
|
||||
override fun serialize(expr: Expression, generator: JsonGenerator, provider: SerializerProvider) = generator.writeString(expr.expr)
|
||||
}
|
||||
|
||||
object ExpressionDeserializer : JsonDeserializer<Expression>() {
|
||||
override fun deserialize(parser: JsonParser, context: DeserializationContext): Expression = Expression(parser.text)
|
||||
}
|
@ -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")
|
@ -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
|
@ -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
|
@ -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
|
||||
|
@ -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
|
@ -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<Unit>() {
|
||||
override fun call() {
|
||||
throw CashException("BOOM!", IllegalStateException("Nope dude!"))
|
||||
}
|
||||
}
|
@ -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
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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.*
|
||||
|
@ -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 {
|
||||
|
@ -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
|
@ -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")
|
@ -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) }
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
package net.corda.finance.contracts.math
|
||||
package net.corda.irs.math
|
||||
|
||||
import java.util.*
|
||||
|
@ -1,4 +1,4 @@
|
||||
package net.corda.finance.contracts.math
|
||||
package net.corda.irs.math
|
||||
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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<Currency>,
|
||||
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<Currency>,
|
||||
|
||||
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)))
|
||||
|
@ -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)
|
||||
|
@ -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<TestCordappImpl> = setOf(FINANCE_CONTRACTS_CORDAPP, FINANCE_WORKFLOWS_CORDAPP)
|
||||
|
@ -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)
|
||||
|
@ -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"
|
||||
|
@ -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.*
|
||||
|
||||
|
@ -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
|
||||
|
@ -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.*
|
||||
|
@ -1,4 +1,4 @@
|
||||
package net.corda.finance.utils
|
||||
package net.corda.explorer
|
||||
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import java.util.*
|
@ -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() {
|
||||
|
@ -1,4 +1,4 @@
|
||||
package net.corda.finance.utils
|
||||
package net.corda.explorer
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
Loading…
Reference in New Issue
Block a user