mirror of
https://github.com/corda/corda.git
synced 2024-12-24 23:26:48 +00:00
Initial checkin for Trade Finance work into experimental branch
This commit is contained in:
parent
7e6c1332e7
commit
ac0d0ec0ec
@ -0,0 +1,165 @@
|
|||||||
|
package com.r3corda.contracts
|
||||||
|
|
||||||
|
import com.r3corda.core.contracts.*
|
||||||
|
import com.r3corda.core.crypto.Party
|
||||||
|
import com.r3corda.core.crypto.SecureHash
|
||||||
|
import com.r3corda.core.crypto.toStringShort
|
||||||
|
import java.security.PublicKey
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.ZoneOffset
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
val ACCOUNTRECEIVABLE_PROGRAM_ID = AccountReceivable()
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class AccountReceivable : Contract {
|
||||||
|
|
||||||
|
enum class StatusEnum {
|
||||||
|
Applied,
|
||||||
|
Issued
|
||||||
|
}
|
||||||
|
|
||||||
|
data class AccountReceivableProperties(
|
||||||
|
val invoiceID: String,
|
||||||
|
val exporter: LocDataStructures.Company,
|
||||||
|
val buyer: LocDataStructures.Company,
|
||||||
|
val currency: Issued<Currency>,
|
||||||
|
val invoiceDate: LocalDate,
|
||||||
|
val invoiceAmount: Amount<Issued<Currency>>,
|
||||||
|
val purchaseDate: LocalDate,
|
||||||
|
val maturityDate: LocalDate,
|
||||||
|
val discountRate: Double // should be a number between 0 and 1.0. 90% = 0.9
|
||||||
|
|
||||||
|
) {
|
||||||
|
|
||||||
|
val purchaseAmount: Amount<Issued<Currency>> = invoiceAmount.times((discountRate * 100).toInt()).div(100)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class State(
|
||||||
|
// technical variables
|
||||||
|
override val owner: PublicKey,
|
||||||
|
val status: StatusEnum,
|
||||||
|
val props: AccountReceivableProperties
|
||||||
|
|
||||||
|
) : OwnableState {
|
||||||
|
override val contract = ACCOUNTRECEIVABLE_PROGRAM_ID
|
||||||
|
|
||||||
|
override val participants: List<PublicKey>
|
||||||
|
get() = listOf(owner)
|
||||||
|
|
||||||
|
override fun toString() = "AR owned by ${owner.toStringShort()})"
|
||||||
|
|
||||||
|
fun checkInvoice(invoice: Invoice.State): Boolean {
|
||||||
|
val arProps = Helper.invoicePropsToARProps(invoice.props, props.discountRate)
|
||||||
|
return props == arProps
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Issue(), copy(owner = newOwner, status = StatusEnum.Issued))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object Helper {
|
||||||
|
fun invoicePropsToARProps(invoiceProps: Invoice.InvoiceProperties, discountRate: Double): AccountReceivableProperties {
|
||||||
|
return AccountReceivable.AccountReceivableProperties(
|
||||||
|
invoiceProps.invoiceID,
|
||||||
|
invoiceProps.seller, invoiceProps.buyer, invoiceProps.goodCurrency,
|
||||||
|
invoiceProps.invoiceDate, invoiceProps.amount, LocalDate.MIN,
|
||||||
|
invoiceProps.payDate, discountRate)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createARFromInvoice(invoice: Invoice.State, discountRate: Double, notary: Party): TransactionState<AccountReceivable.State> {
|
||||||
|
val arProps = invoicePropsToARProps(invoice.props, discountRate)
|
||||||
|
val ar = AccountReceivable.State(invoice.owner.owningKey, StatusEnum.Applied, arProps)
|
||||||
|
return TransactionState<AccountReceivable.State>(ar, notary)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generateAR(invoice: StateAndRef<Invoice.State>, discountRate: Double, notary: Party): TransactionBuilder {
|
||||||
|
if (invoice.state.data.assigned) {
|
||||||
|
throw IllegalArgumentException("Cannot build AR with an already assigned invoice")
|
||||||
|
}
|
||||||
|
val ar = createARFromInvoice(invoice.state.data, discountRate, notary)
|
||||||
|
val tx = TransactionType.General.Builder()
|
||||||
|
tx.addInputState(invoice)
|
||||||
|
tx.addOutputState(invoice.state.data.copy(assigned = true))
|
||||||
|
tx.addCommand(Invoice.Commands.Assign(), invoice.state.data.owner.owningKey)
|
||||||
|
tx.addOutputState(ar)
|
||||||
|
tx.addCommand(AccountReceivable.Commands.Apply(), invoice.state.data.owner.owningKey)
|
||||||
|
return tx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Commands : CommandData {
|
||||||
|
// Seller offer AR to bank
|
||||||
|
class Apply : TypeOnlyCommandData(), Commands
|
||||||
|
|
||||||
|
// Bank check the paper, and accept or reject
|
||||||
|
class Issue : TypeOnlyCommandData(), Commands
|
||||||
|
|
||||||
|
// When buyer paid to bank, case close
|
||||||
|
class Extinguish : TypeOnlyCommandData(), Commands
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun verify(tx: TransactionForContract) {
|
||||||
|
val command = tx.commands.requireSingleCommand<AccountReceivable.Commands>()
|
||||||
|
|
||||||
|
val time = tx.commands.getTimestampByName("Notary Service", "Seller")?.midpoint ?:
|
||||||
|
throw IllegalArgumentException("must be timestamped")
|
||||||
|
|
||||||
|
when (command.value) {
|
||||||
|
is AccountReceivable.Commands.Apply -> {
|
||||||
|
val invoiceCommand = tx.commands.requireSingleCommand<Invoice.Commands>()
|
||||||
|
if (invoiceCommand.value !is Invoice.Commands.Assign) {
|
||||||
|
throw IllegalArgumentException("The invoice command associated must be 'Assign'")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tx.inputs.size != 1) {
|
||||||
|
throw IllegalArgumentException("There must be an input Invoice state")
|
||||||
|
}
|
||||||
|
val inputInvoice: Invoice.State = tx.inputs.filterIsInstance<Invoice.State>().single()
|
||||||
|
if (tx.outputs.size != 2) {
|
||||||
|
throw IllegalArgumentException("There must be two output states")
|
||||||
|
}
|
||||||
|
val newAR: AccountReceivable.State = tx.outputs.filterIsInstance<AccountReceivable.State>().single()
|
||||||
|
|
||||||
|
requireThat {
|
||||||
|
"AR state must be applied" by (newAR.status == StatusEnum.Applied)
|
||||||
|
"AR properties must match input invoice" by newAR.checkInvoice(inputInvoice)
|
||||||
|
"The discount factor is invalid" by (newAR.props.discountRate >= 0.0 &&
|
||||||
|
newAR.props.discountRate <= 1.0)
|
||||||
|
"the payment date must be in the the future" by
|
||||||
|
(newAR.props.maturityDate.atStartOfDay().toInstant(ZoneOffset.UTC) >= time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is AccountReceivable.Commands.Issue -> {
|
||||||
|
val oldAR: AccountReceivable.State = tx.inputs.filterIsInstance<AccountReceivable.State>().single()
|
||||||
|
val newAR: AccountReceivable.State = tx.outputs.filterIsInstance<AccountReceivable.State>().single()
|
||||||
|
|
||||||
|
requireThat {
|
||||||
|
"input status must be applied" by (oldAR.status == StatusEnum.Applied)
|
||||||
|
"output status must be issued" by (newAR.status == StatusEnum.Issued)
|
||||||
|
"properties must match" by (newAR.props == oldAR.props)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is AccountReceivable.Commands.Extinguish -> {
|
||||||
|
val oldAR: AccountReceivable.State = tx.inputs.filterIsInstance<AccountReceivable.State>().single()
|
||||||
|
val newAR: AccountReceivable.State? = tx.outputs.filterIsInstance<AccountReceivable.State>().singleOrNull()
|
||||||
|
|
||||||
|
requireThat {
|
||||||
|
"input status must be issued" by (oldAR.status == StatusEnum.Issued)
|
||||||
|
"output state must not exist" by (newAR == null)
|
||||||
|
"the payment date must be today or in the the past" by
|
||||||
|
(oldAR.props.maturityDate.atStartOfDay().toInstant(ZoneOffset.UTC) <= time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// legal Prose
|
||||||
|
override val legalContractReference: SecureHash = SecureHash.sha256("AccountReceivable")
|
||||||
|
}
|
@ -0,0 +1,137 @@
|
|||||||
|
package com.r3corda.contracts
|
||||||
|
|
||||||
|
import com.r3corda.core.contracts.*
|
||||||
|
import com.r3corda.core.crypto.Party
|
||||||
|
import com.r3corda.core.crypto.SecureHash
|
||||||
|
import java.security.PublicKey
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Bill of Lading Agreement
|
||||||
|
//
|
||||||
|
|
||||||
|
val BILL_OF_LADING_PROGRAM_ID = BillOfLadingAgreement()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A bill of lading is a standard-form document. It is transferable by endorsement (or by lawful transfer of possession)
|
||||||
|
* and is a receipt from shipping company regarding the number of packages with a particular weight and markings and a
|
||||||
|
* contract for the transportation of same to a port of destination mentioned therein.
|
||||||
|
*
|
||||||
|
* An order bill of lading is used when shipping merchandise prior to payment, requiring a carrier to deliver the
|
||||||
|
* merchandise to the importer, and at the endorsement of the exporter the carrier may transfer title to the importer.
|
||||||
|
* Endorsed order bills of lading can be traded as a security or serve as collateral against debt obligations.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
class BillOfLadingAgreement : Contract {
|
||||||
|
|
||||||
|
data class BillOfLadingProperties(
|
||||||
|
val billOfLadingID: String,
|
||||||
|
val issueDate: LocalDate,
|
||||||
|
val carrierOwner: Party,
|
||||||
|
val nameOfVessel: String,
|
||||||
|
val descriptionOfGoods: List<LocDataStructures.Good>,
|
||||||
|
val portOfLoading: LocDataStructures.Port,
|
||||||
|
val portOfDischarge: LocDataStructures.Port,
|
||||||
|
val grossWeight: LocDataStructures.Weight,
|
||||||
|
val dateOfShipment: LocalDate?,
|
||||||
|
val shipper: LocDataStructures.Company?,
|
||||||
|
val notify: LocDataStructures.Person?,
|
||||||
|
val consignee: LocDataStructures.Company?
|
||||||
|
) {}
|
||||||
|
|
||||||
|
data class State(
|
||||||
|
// technical variables
|
||||||
|
override val owner: PublicKey,
|
||||||
|
val beneficiary: Party,
|
||||||
|
val props: BillOfLadingProperties
|
||||||
|
|
||||||
|
) : OwnableState {
|
||||||
|
override val participants: List<PublicKey>
|
||||||
|
get() = listOf(owner)
|
||||||
|
|
||||||
|
override fun withNewOwner(newOwner: PublicKey): Pair<CommandData, OwnableState> {
|
||||||
|
return Pair(Commands.TransferPossession(), copy(owner = newOwner))
|
||||||
|
}
|
||||||
|
|
||||||
|
override val contract = BILL_OF_LADING_PROGRAM_ID
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Commands : CommandData {
|
||||||
|
class IssueBL : TypeOnlyCommandData(), Commands
|
||||||
|
class TransferAndEndorseBL : TypeOnlyCommandData(), Commands
|
||||||
|
class TransferPossession : TypeOnlyCommandData(), Commands
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The Invoice contract needs to handle three commands
|
||||||
|
* 1: IssueBL --
|
||||||
|
* 2: TransferAndEndorseBL --
|
||||||
|
* 3: TransferPossession --
|
||||||
|
*/
|
||||||
|
override fun verify(tx: TransactionForContract) {
|
||||||
|
val command = tx.commands.requireSingleCommand<BillOfLadingAgreement.Commands>()
|
||||||
|
|
||||||
|
val time = tx.commands.getTimestampByName("Notary Service")?.midpoint
|
||||||
|
if (time == null) throw IllegalArgumentException("must be timestamped")
|
||||||
|
|
||||||
|
val txOutputStates: List<BillOfLadingAgreement.State> = tx.outputs.filterIsInstance<BillOfLadingAgreement.State>()
|
||||||
|
val txInputStates: List<BillOfLadingAgreement.State> = tx.inputs.filterIsInstance<BillOfLadingAgreement.State>()
|
||||||
|
|
||||||
|
when (command.value) {
|
||||||
|
|
||||||
|
is Commands.IssueBL -> {
|
||||||
|
requireThat {
|
||||||
|
"there is no input state" by txInputStates.isEmpty()
|
||||||
|
"the transaction is signed by the carrier" by (command.signers.contains(txOutputStates.single().props.carrierOwner.owningKey))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is Commands.TransferAndEndorseBL -> {
|
||||||
|
requireThat {
|
||||||
|
"the transaction is signed by the beneficiary" by (command.signers.contains(txInputStates.single().beneficiary.owningKey))
|
||||||
|
"the transaction is signed by the state object owner" by (command.signers.contains(txInputStates.single().owner))
|
||||||
|
"the bill of lading agreement properties are unchanged" by (txInputStates.single().props == txOutputStates.single().props)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is Commands.TransferPossession -> {
|
||||||
|
requireThat {
|
||||||
|
"the transaction is signed by the state object owner" by (command.signers.contains(txInputStates.single().owner))
|
||||||
|
//"the state object owner has been updated" by (txInputStates.single().owner != txOutputStates.single().owner)
|
||||||
|
"the beneficiary is unchanged" by (txInputStates.single().beneficiary == txOutputStates.single().beneficiary)
|
||||||
|
"the bill of lading agreement properties are unchanged" by (txInputStates.single().props == txOutputStates.single().props)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val legalContractReference: SecureHash = SecureHash.sha256("https://en.wikipedia.org/wiki/Bill_of_lading")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a transaction that issues a Bill of Lading Agreement
|
||||||
|
*/
|
||||||
|
fun generateIssue(owner: PublicKey, beneficiary: Party, props: BillOfLadingProperties, notary: Party? = null): TransactionBuilder {
|
||||||
|
val state = State(owner, beneficiary, props)
|
||||||
|
return TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.IssueBL(), props.carrierOwner.owningKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the given partial transaction with an input/output/command to reassign ownership of the paper.
|
||||||
|
*/
|
||||||
|
fun generateTransferAndEndorse(tx: TransactionBuilder, BoL: StateAndRef<State>, newOwner: PublicKey, newBeneficiary: Party) {
|
||||||
|
tx.addInputState(BoL)
|
||||||
|
tx.addOutputState(BoL.state.data.copy(owner = newOwner, beneficiary = newBeneficiary))
|
||||||
|
val signers: List<PublicKey> = listOf(BoL.state.data.owner, BoL.state.data.beneficiary.owningKey)
|
||||||
|
tx.addCommand(Commands.TransferAndEndorseBL(), signers)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the given partial transaction with an input/output/command to reassign ownership of the paper.
|
||||||
|
*/
|
||||||
|
fun generateTransferPossession(tx: TransactionBuilder, BoL: StateAndRef<State>, newOwner: PublicKey) {
|
||||||
|
tx.addInputState(BoL)
|
||||||
|
tx.addOutputState(BoL.state.data.copy(owner = newOwner))
|
||||||
|
// tx.addOutputState(BoL.state.data.copy().withNewOwner(newOwner))
|
||||||
|
tx.addCommand(Commands.TransferPossession(), BoL.state.data.owner)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
167
experimental/src/main/kotlin/com/r3corda/contracts/Invoice.kt
Normal file
167
experimental/src/main/kotlin/com/r3corda/contracts/Invoice.kt
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
package com.r3corda.contracts
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||||
|
import com.r3corda.core.contracts.*
|
||||||
|
import com.r3corda.core.crypto.Party
|
||||||
|
import com.r3corda.core.crypto.SecureHash
|
||||||
|
import java.security.PublicKey
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.ZoneOffset
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Invoice
|
||||||
|
//
|
||||||
|
|
||||||
|
val INVOICE_PROGRAM_ID = Invoice()
|
||||||
|
|
||||||
|
// TODO: Any custom exceptions needed?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An invoice is a document that describes a trade between a buyer and a seller. It is issued on a particular date,
|
||||||
|
* it lists goods being sold by the seller, the cost of each good and the total amount owed by the buyer and when
|
||||||
|
* the buyer expects to be paid by.
|
||||||
|
*
|
||||||
|
* In the trade finance world, invoices are used to create other contracts (for example AccountsReceivable), newly
|
||||||
|
* created invoices start off with a status of "unassigned", once they're used to create other contracts the status
|
||||||
|
* is changed to "assigned". This ensures that an invoice is used only once when creating a financial product like
|
||||||
|
* AccountsReceivable.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
class Invoice : Contract {
|
||||||
|
|
||||||
|
|
||||||
|
data class InvoiceProperties(
|
||||||
|
val invoiceID: String,
|
||||||
|
val seller: LocDataStructures.Company,
|
||||||
|
val buyer: LocDataStructures.Company,
|
||||||
|
val invoiceDate: LocalDate,
|
||||||
|
val term: Long,
|
||||||
|
val goods: List<LocDataStructures.PricedGood> = ArrayList()
|
||||||
|
) {
|
||||||
|
init {
|
||||||
|
require(term > 0) { "the term must be a positive number" }
|
||||||
|
require(goods.isNotEmpty()) { "there must be goods assigned to the invoice" }
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns the single currency used by the goods list
|
||||||
|
val goodCurrency: Issued<Currency> get() = goods.map { it.unitPrice.token }.distinct().single()
|
||||||
|
|
||||||
|
// iterate over the goods list and sum up the price for each
|
||||||
|
val amount: Amount<Issued<Currency>> get() = goods.map { it.totalPrice() }.sumOrZero(goodCurrency)
|
||||||
|
|
||||||
|
// add term to invoice date to determine the payDate
|
||||||
|
val payDate: LocalDate get() {
|
||||||
|
return invoiceDate.plusDays(term)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
data class State(
|
||||||
|
// technical variables
|
||||||
|
val owner: Party,
|
||||||
|
val buyer: Party,
|
||||||
|
val assigned: Boolean,
|
||||||
|
val props: InvoiceProperties
|
||||||
|
|
||||||
|
) : LinearState {
|
||||||
|
|
||||||
|
override val contract = INVOICE_PROGRAM_ID
|
||||||
|
|
||||||
|
override val participants: List<PublicKey>
|
||||||
|
get() = listOf(owner.owningKey)
|
||||||
|
|
||||||
|
// returns true when the actual business properties of the
|
||||||
|
// invoice is modified
|
||||||
|
fun propertiesChanged(otherState: State): Boolean {
|
||||||
|
return (props != otherState.props)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generateInvoice(notary: Party? = null): TransactionBuilder = Invoice().generateInvoice(props, owner, buyer, notary)
|
||||||
|
|
||||||
|
// iterate over the goods list and sum up the price for each
|
||||||
|
val amount: Amount<Issued<Currency>> get() = props.amount
|
||||||
|
|
||||||
|
override val thread = SecureHash.Companion.sha256(props.invoiceID)
|
||||||
|
|
||||||
|
override fun isRelevant(ourKeys: Set<PublicKey>): Boolean {
|
||||||
|
return owner.owningKey in ourKeys || buyer.owningKey in ourKeys
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generateInvoice(props: InvoiceProperties, owner: Party, buyer: Party, notary: Party? = null): TransactionBuilder {
|
||||||
|
val state = State(owner, buyer, false, props)
|
||||||
|
return TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.Issue(), listOf(owner.owningKey)))
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Commands : CommandData {
|
||||||
|
class Issue : TypeOnlyCommandData(), Commands
|
||||||
|
class Assign : TypeOnlyCommandData(), Commands
|
||||||
|
class Extinguish : TypeOnlyCommandData(), Commands
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The Invoice contract needs to handle three commands
|
||||||
|
* 1: Issue -- the creation of the Invoice contract. We need to confirm that the correct
|
||||||
|
* party signed the contract and that the relevant fields are populated with valid data.
|
||||||
|
* 2: Assign -- the invoice is used to create another type of Contract. The assigned boolean has to change from
|
||||||
|
* false to true.
|
||||||
|
* 3: Extinguish -- the invoice is deleted. Proper signing is required.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
override fun verify(tx: TransactionForContract) {
|
||||||
|
val command = tx.commands.requireSingleCommand<Invoice.Commands>()
|
||||||
|
|
||||||
|
val time = tx.commands.getTimestampByName("Notary Service", "Seller")?.midpoint ?:
|
||||||
|
throw IllegalArgumentException("must be timestamped")
|
||||||
|
|
||||||
|
when (command.value) {
|
||||||
|
is Commands.Issue -> {
|
||||||
|
if (tx.outputs.size != 1) {
|
||||||
|
throw IllegalArgumentException("Failed requirement: during issuance of the invoice, only " +
|
||||||
|
"one output invoice state should be include in the transaction. " +
|
||||||
|
"Number of output states included was " + tx.outputs.size)
|
||||||
|
}
|
||||||
|
val issueOutput: Invoice.State = tx.outputs.filterIsInstance<Invoice.State>().single()
|
||||||
|
|
||||||
|
requireThat {
|
||||||
|
"there is no input state" by tx.inputs.filterIsInstance<State>().isEmpty()
|
||||||
|
"the transaction is signed by the invoice owner" by (command.signers.contains(issueOutput.owner.owningKey))
|
||||||
|
"the buyer and seller must be different" by (issueOutput.props.buyer.name != issueOutput.props.seller.name)
|
||||||
|
"the invoice must not be assigned" by (issueOutput.assigned == false)
|
||||||
|
"the invoice ID must not be blank" by (issueOutput.props.invoiceID.length > 0)
|
||||||
|
"the term must be a positive number" by (issueOutput.props.term > 0)
|
||||||
|
"the payment date must be in the future" by (issueOutput.props.payDate.atStartOfDay().toInstant(ZoneOffset.UTC) > time)
|
||||||
|
"there must be goods associated with the invoice" by (issueOutput.props.goods.isNotEmpty())
|
||||||
|
"the invoice amount must be non-zero" by (issueOutput.amount.quantity > 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is Commands.Assign -> {
|
||||||
|
val assignInput: Invoice.State = tx.inputs.filterIsInstance<Invoice.State>().single()
|
||||||
|
val assignOutput: Invoice.State = tx.outputs.filterIsInstance<Invoice.State>().single()
|
||||||
|
|
||||||
|
requireThat {
|
||||||
|
"input state owner must be the same as the output state owner" by (assignInput.owner == assignOutput.owner)
|
||||||
|
"the transaction must be signed by the owner" by (command.signers.contains(assignInput.owner.owningKey))
|
||||||
|
"the invoice properties must remain unchanged" by (!assignOutput.propertiesChanged(assignInput))
|
||||||
|
"the input invoice must not be assigned" by (assignInput.assigned == false)
|
||||||
|
"the output invoice must be assigned" by (assignOutput.assigned == true)
|
||||||
|
"the payment date must be in the future" by (assignInput.props.payDate.atStartOfDay().toInstant(ZoneOffset.UTC) > time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is Commands.Extinguish -> {
|
||||||
|
val extinguishInput: Invoice.State = tx.inputs.filterIsInstance<Invoice.State>().single()
|
||||||
|
val extinguishOutput: Invoice.State? = tx.outputs.filterIsInstance<Invoice.State>().singleOrNull()
|
||||||
|
|
||||||
|
requireThat {
|
||||||
|
"there shouldn't be an output state" by (extinguishOutput == null)
|
||||||
|
"the transaction must be signed by the owner" by (command.signers.contains(extinguishInput.owner.owningKey))
|
||||||
|
// "the payment date must be today or in the past" by (extinguishInput.props.payDate.atStartOfDay().toInstant(ZoneOffset.UTC) < time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val legalContractReference: SecureHash = SecureHash.sha256("Invoice")
|
||||||
|
}
|
@ -0,0 +1,154 @@
|
|||||||
|
package com.r3corda.contracts
|
||||||
|
|
||||||
|
import com.r3corda.core.contracts.*
|
||||||
|
import com.r3corda.core.crypto.NullPublicKey
|
||||||
|
import com.r3corda.core.crypto.Party
|
||||||
|
import com.r3corda.core.crypto.SecureHash
|
||||||
|
import java.security.PublicKey
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.Period
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Letter of Credit Application
|
||||||
|
//
|
||||||
|
|
||||||
|
// Just a fake program identifier for now. In a real system it could be, for instance, the hash of the program bytecode.
|
||||||
|
val LC_APPLICATION_PROGRAM_ID = LCApplication()
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class LCApplication : Contract {
|
||||||
|
// TODO: should reference the content of the legal agreement, not its URI
|
||||||
|
override val legalContractReference: SecureHash = SecureHash.sha256("Letter of Credit Application")
|
||||||
|
|
||||||
|
override fun verify(tx: TransactionForContract) {
|
||||||
|
val command = tx.commands.requireSingleCommand<LCApplication.Commands>()
|
||||||
|
val inputs = tx.inputs.filterIsInstance<State>()
|
||||||
|
val outputs = tx.outputs.filterIsInstance<State>()
|
||||||
|
|
||||||
|
// Here, we match acceptable timestamp authorities by name. The list of acceptable TSAs (oracles) must be
|
||||||
|
// hard coded into the contract because otherwise we could fail to gain consensus, if nodes disagree about
|
||||||
|
// who or what is a trusted authority.
|
||||||
|
tx.commands.getTimestampByName("Mock Company 0", "Notary Service", "Bank A")
|
||||||
|
|
||||||
|
when (command.value) {
|
||||||
|
is Commands.ApplyForLC -> {
|
||||||
|
verifyApply(inputs, outputs, command as AuthenticatedObject<Commands.ApplyForLC>, tx)
|
||||||
|
}
|
||||||
|
is Commands.Approve -> {
|
||||||
|
verifyApprove(inputs, outputs, command as AuthenticatedObject<Commands.Approve>, tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Think about how to evolve contracts over time with new commands.
|
||||||
|
else -> throw IllegalArgumentException("Unrecognised command")
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun verifyApply(inputs: List<State>, outputs: List<State>, command: AuthenticatedObject<Commands.ApplyForLC>, tx: TransactionForContract) {
|
||||||
|
val output = outputs.single()
|
||||||
|
val applicant = output.props.applicant
|
||||||
|
|
||||||
|
requireThat {
|
||||||
|
//TODO - Is this required???
|
||||||
|
"the owner must be the issuer" by output.props.issuer.owningKey.equals(output.owner)
|
||||||
|
"there is no input state" by inputs.isEmpty()
|
||||||
|
"the transaction is signed by the applicant" by (command.signers.contains(applicant.owningKey))
|
||||||
|
//TODO - sales contract attached
|
||||||
|
//TODO - purchase order attached
|
||||||
|
//TODO - application confirms to required template
|
||||||
|
"the state is propagated" by (outputs.size == 1)
|
||||||
|
"the output status must be pending issuer review" by (output.status.equals(Status.PENDING_ISSUER_REVIEW))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun verifyApprove(inputs: List<State>, outputs: List<State>, command: AuthenticatedObject<Commands.Approve>, tx: TransactionForContract) {
|
||||||
|
val input = inputs.single()
|
||||||
|
val output = outputs.single()
|
||||||
|
val issuer = output.owner
|
||||||
|
|
||||||
|
requireThat {
|
||||||
|
//TODO - signed by owner
|
||||||
|
"the transaction is signed by the issuer bank (object owner)" by (command.signers.contains(issuer))
|
||||||
|
"the input status must be pending issuer review" by (input.status.equals(Status.PENDING_ISSUER_REVIEW))
|
||||||
|
"the output status must be approved" by (output.status.equals(Status.APPROVED))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Status {
|
||||||
|
PENDING_ISSUER_REVIEW,
|
||||||
|
APPROVED,
|
||||||
|
REJECTED
|
||||||
|
}
|
||||||
|
|
||||||
|
data class LCApplicationProperties(
|
||||||
|
val letterOfCreditApplicationID: String,
|
||||||
|
val applicationDate: LocalDate,
|
||||||
|
val typeCredit: LocDataStructures.CreditType,
|
||||||
|
val issuer: Party,
|
||||||
|
val beneficiary: Party,
|
||||||
|
val applicant: Party,
|
||||||
|
val expiryDate: LocalDate,
|
||||||
|
val portLoading: LocDataStructures.Port,
|
||||||
|
val portDischarge: LocDataStructures.Port,
|
||||||
|
val placePresentation: LocDataStructures.Location,
|
||||||
|
val lastShipmentDate: LocalDate,
|
||||||
|
val periodPresentation: Period,
|
||||||
|
val goods: List<LocDataStructures.PricedGood> = ArrayList(),
|
||||||
|
val documentsRequired: List<String> = ArrayList(),
|
||||||
|
val invoiceRef: StateRef,
|
||||||
|
val amount: Amount<Issued<Currency>>
|
||||||
|
) {
|
||||||
|
init {
|
||||||
|
if (periodPresentation == null || periodPresentation.isZero) {
|
||||||
|
// TODO: set default value???
|
||||||
|
// periodPresentation = Period.ofDays(21)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class State(
|
||||||
|
val owner: PublicKey,
|
||||||
|
val status: Status,
|
||||||
|
val props: LCApplicationProperties
|
||||||
|
) : ContractState {
|
||||||
|
|
||||||
|
override val contract = LC_APPLICATION_PROGRAM_ID
|
||||||
|
|
||||||
|
override val participants: List<PublicKey>
|
||||||
|
get() = listOf(owner)
|
||||||
|
|
||||||
|
// returns true when the actual business properties of the
|
||||||
|
// invoice is modified
|
||||||
|
fun propertiesChanged(otherState: State): Boolean {
|
||||||
|
return (props != otherState.props)
|
||||||
|
}
|
||||||
|
|
||||||
|
// iterate over the goods list and sum up the price for each
|
||||||
|
fun withoutOwner() = copy(owner = NullPublicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generateApply(props: LCApplicationProperties, notary: Party, purchaseOrder: Attachment): TransactionBuilder {
|
||||||
|
val state = State(props.issuer.owningKey, Status.PENDING_ISSUER_REVIEW, props)
|
||||||
|
val txBuilder = TransactionType.General.Builder().withItems(state, Command(Commands.ApplyForLC(), props.applicant.owningKey))
|
||||||
|
txBuilder.addAttachment(purchaseOrder.id)
|
||||||
|
return txBuilder
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generateApprove(tx: TransactionBuilder, application: StateAndRef<LCApplication.State>) {
|
||||||
|
tx.addInputState(application)
|
||||||
|
tx.addOutputState(application.state.data.copy(status = Status.APPROVED))
|
||||||
|
tx.addCommand(Commands.Approve(), application.state.data.owner)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Commands : CommandData {
|
||||||
|
class ApplyForLC : TypeOnlyCommandData(), Commands
|
||||||
|
class Approve : TypeOnlyCommandData(), Commands
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
149
experimental/src/main/kotlin/com/r3corda/contracts/LOC.kt
Normal file
149
experimental/src/main/kotlin/com/r3corda/contracts/LOC.kt
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
package com.r3corda.contracts
|
||||||
|
|
||||||
|
import com.r3corda.contracts.asset.sumCashBy
|
||||||
|
import com.r3corda.core.contracts.*
|
||||||
|
import com.r3corda.core.crypto.Party
|
||||||
|
import com.r3corda.core.crypto.SecureHash
|
||||||
|
import java.security.PublicKey
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.Period
|
||||||
|
import java.time.ZoneOffset
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
val LOC_PROGRAM_ID = LOC()
|
||||||
|
|
||||||
|
/** LOC contract - consists of the following commands 1. Issue 2. DemandPresentation 3. Termination ***/
|
||||||
|
|
||||||
|
|
||||||
|
class LOC : Contract {
|
||||||
|
|
||||||
|
|
||||||
|
data class Company(
|
||||||
|
val name: String,
|
||||||
|
val address: String,
|
||||||
|
val phone: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class LOCProperties(
|
||||||
|
val letterOfCreditID: String,
|
||||||
|
val applicationDate: LocalDate,
|
||||||
|
val issueDate: LocalDate,
|
||||||
|
val typeCredit: LocDataStructures.CreditType,
|
||||||
|
val amount: Amount<Issued<Currency>>,
|
||||||
|
val invoiceRef: StateRef,
|
||||||
|
val expiryDate: LocalDate,
|
||||||
|
val portLoading: LocDataStructures.Port,
|
||||||
|
val portDischarge: LocDataStructures.Port,
|
||||||
|
val descriptionGoods: List<LocDataStructures.PricedGood>,
|
||||||
|
val placePresentation: LocDataStructures.Location,
|
||||||
|
val latestShip: LocalDate,
|
||||||
|
val periodPresentation: Period,
|
||||||
|
val beneficiary: Party,
|
||||||
|
val issuingbank: Party,
|
||||||
|
val appplicant: Party
|
||||||
|
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
data class State(
|
||||||
|
// technical variables
|
||||||
|
val beneficiaryPaid: Boolean,
|
||||||
|
val issued: Boolean,
|
||||||
|
val terminated: Boolean,
|
||||||
|
val props: LOC.LOCProperties
|
||||||
|
|
||||||
|
) : ContractState {
|
||||||
|
override val contract = LOC_PROGRAM_ID
|
||||||
|
|
||||||
|
override val participants: List<PublicKey>
|
||||||
|
get() = listOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Commands : CommandData {
|
||||||
|
class Issuance : TypeOnlyCommandData(), Commands
|
||||||
|
class DemandPresentation : TypeOnlyCommandData(), Commands
|
||||||
|
class Termination : TypeOnlyCommandData(), Commands
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun verify(tx: TransactionForContract) {
|
||||||
|
val command = tx.commands.requireSingleCommand<LOC.Commands>()
|
||||||
|
|
||||||
|
val time = tx.commands.getTimestampByName("Notary Service")?.midpoint
|
||||||
|
if (time == null) throw IllegalArgumentException("must be timestamped")
|
||||||
|
|
||||||
|
when (command.value) {
|
||||||
|
is Commands.Issuance -> {
|
||||||
|
|
||||||
|
// val LOCappInput: LOCapp.State = tx.inStates.filterIsInstance<LOCapp.State>().single()
|
||||||
|
val LOCissueOutput: LOC.State = tx.outputs.filterIsInstance<LOC.State>().single()
|
||||||
|
|
||||||
|
requireThat {
|
||||||
|
// "there is no input state" by !tx.inStates.filterIsInstance<State>().isEmpty() TODO: verify if LOC application is submitted
|
||||||
|
//"LOC application has not been submitted" by (tx.inStates.filterIsInstance<LOCapp.State>().count() == 1)
|
||||||
|
"the transaction is not signed by the issuing bank" by (command.signers.contains(LOCissueOutput.props.issuingbank.owningKey))
|
||||||
|
"the LOC must be Issued" by (LOCissueOutput.issued == true)
|
||||||
|
"Demand Presentation must not be preformed successfully" by (LOCissueOutput.beneficiaryPaid == false)
|
||||||
|
"LOC must not be terminated" by (LOCissueOutput.terminated == false)
|
||||||
|
"the period of presentation must be a positive number" by (!LOCissueOutput.props.periodPresentation.isNegative && !LOCissueOutput.props.periodPresentation.isZero)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is Commands.DemandPresentation -> {
|
||||||
|
|
||||||
|
|
||||||
|
val LOCInput: LOC.State = tx.inputs.filterIsInstance<LOC.State>().single()
|
||||||
|
val invoiceInput: Invoice.State = tx.inputs.filterIsInstance<Invoice.State>().single()
|
||||||
|
|
||||||
|
val LOCdemandOutput: LOC.State = tx.outputs.filterIsInstance<LOC.State>().single()
|
||||||
|
val BOLtransferOutput: BillOfLadingAgreement.State = tx.outputs.filterIsInstance<BillOfLadingAgreement.State>().single()
|
||||||
|
|
||||||
|
val CashpayOutput = tx.outputs.sumCashBy(LOCdemandOutput.props.beneficiary.owningKey)
|
||||||
|
|
||||||
|
requireThat {
|
||||||
|
|
||||||
|
"there is no input state" by !tx.inputs.filterIsInstance<State>().isEmpty()
|
||||||
|
"the transaction is signed by the issuing bank" by (command.signers.contains(LOCdemandOutput.props.issuingbank.owningKey))
|
||||||
|
"the transaction is signed by the Beneficiary" by (command.signers.contains(LOCdemandOutput.props.beneficiary.owningKey))
|
||||||
|
"the LOC properties do not remain the same" by (LOCInput.props.equals(LOCdemandOutput.props))
|
||||||
|
"the LOC expiry date has passed" by (LOCdemandOutput.props.expiryDate.atStartOfDay().toInstant(ZoneOffset.UTC) > time)
|
||||||
|
"the shipment is late" by (LOCdemandOutput.props.latestShip > (BOLtransferOutput.props.dateOfShipment ?: BOLtransferOutput.props.issueDate))
|
||||||
|
"the cash state has not been transferred" by (CashpayOutput.token.equals(invoiceInput.amount.token) && CashpayOutput.quantity >= (invoiceInput.amount.quantity))
|
||||||
|
"the bill of lading has not been transferred" by (LOCdemandOutput.props.appplicant.owningKey.equals(BOLtransferOutput.beneficiary.owningKey))
|
||||||
|
"the beneficiary has not been paid, status not changed" by (LOCdemandOutput.beneficiaryPaid == true)
|
||||||
|
"the LOC must be Issued" by (LOCdemandOutput.issued == true)
|
||||||
|
"LOC must not be terminated" by (LOCdemandOutput.terminated == false)
|
||||||
|
// "the presentation is late" by (time <= (LOCdemandOutput.props.periodPresentation.addTo(LOCdemandOutput.props.issueDate) as LocalDate).atStartOfDay().toInstant(ZoneOffset.UTC) )
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is Commands.Termination -> {
|
||||||
|
|
||||||
|
val LOCterminateOutput: LOC.State = tx.outputs.filterIsInstance<LOC.State>().single()
|
||||||
|
//val CashpayOutput2: Cash.State = tx.outputs.filterIsInstance<Cash.State>().single()
|
||||||
|
val CashpayOutput = tx.outputs.sumCashBy(LOCterminateOutput.props.issuingbank.owningKey)
|
||||||
|
|
||||||
|
val LOCinput: LOC.State = tx.inputs.filterIsInstance<LOC.State>().single()
|
||||||
|
|
||||||
|
requireThat {
|
||||||
|
"the transaction is signed by the issuing bank" by (command.signers.contains(LOCterminateOutput.props.issuingbank.owningKey))
|
||||||
|
//"the transaction is signed by the applicant" by (command.signers.contains(LOCterminateOutput.props.appplicant.owningKey))
|
||||||
|
"the cash state has not been transferred" by (CashpayOutput.token.equals(LOCterminateOutput.props.amount.token) && CashpayOutput.quantity >= (LOCterminateOutput.props.amount.quantity))
|
||||||
|
"the beneficiary has not been paid, status not changed" by (LOCterminateOutput.beneficiaryPaid == true)
|
||||||
|
"the LOC must be Issued" by (LOCterminateOutput.issued == true)
|
||||||
|
"LOC should be terminated" by (LOCterminateOutput.terminated == true)
|
||||||
|
"the LOC properties do not remain the same" by (LOCinput.props.equals(LOCterminateOutput.props))
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override val legalContractReference: SecureHash = SecureHash.sha256("LOC")
|
||||||
|
|
||||||
|
fun generateIssue(beneficiaryPaid: Boolean, issued: Boolean, terminated: Boolean, props: LOCProperties, notary: Party): TransactionBuilder {
|
||||||
|
val state = State(beneficiaryPaid, issued, terminated, props)
|
||||||
|
return TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.Issuance(), props.issuingbank.owningKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,87 @@
|
|||||||
|
package com.r3corda.contracts
|
||||||
|
|
||||||
|
import com.r3corda.core.contracts.Amount
|
||||||
|
import com.r3corda.core.contracts.Issued
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by N992551 on 30.06.2016.
|
||||||
|
*/
|
||||||
|
|
||||||
|
object LocDataStructures {
|
||||||
|
enum class WeightUnit {
|
||||||
|
KG,
|
||||||
|
LBS
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Weight(
|
||||||
|
val quantity: Double,
|
||||||
|
val unit: LocDataStructures.WeightUnit
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Company(
|
||||||
|
val name: String,
|
||||||
|
val address: String,
|
||||||
|
val phone: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Person(
|
||||||
|
val name: String,
|
||||||
|
val address: String,
|
||||||
|
val phone: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Port(
|
||||||
|
val country: String,
|
||||||
|
val city: String,
|
||||||
|
val address: String?,
|
||||||
|
val name: String?,
|
||||||
|
val state: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Location(
|
||||||
|
val country: String,
|
||||||
|
val state: String?,
|
||||||
|
val city: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Good(
|
||||||
|
val description: String,
|
||||||
|
val quantity: Int,
|
||||||
|
val grossWeight: LocDataStructures.Weight?
|
||||||
|
) {
|
||||||
|
init {
|
||||||
|
require(quantity > 0) { "The good quantity must be a positive value." }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class PricedGood(
|
||||||
|
val description: String,
|
||||||
|
val purchaseOrderRef: String?,
|
||||||
|
val quantity: Int,
|
||||||
|
val unitPrice: Amount<Issued<Currency>>,
|
||||||
|
val grossWeight: LocDataStructures.Weight?
|
||||||
|
) {
|
||||||
|
init {
|
||||||
|
require(quantity > 0) { "The good quantity must be a positive value." }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun totalPrice(): Amount<Issued<Currency>> {
|
||||||
|
return unitPrice.times(quantity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class CreditType {
|
||||||
|
//TODO: There are a lot of types
|
||||||
|
SIGHT,
|
||||||
|
DEFERRED_PAYMENT,
|
||||||
|
ACCEPTANCE,
|
||||||
|
NEGOTIABLE_CREDIT,
|
||||||
|
TRANSFERABLE,
|
||||||
|
STANDBY,
|
||||||
|
REVOLVING,
|
||||||
|
RED_CLAUSE,
|
||||||
|
GREEN_CLAUSE
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user