mirror of
https://github.com/corda/corda.git
synced 2025-05-31 06:31:08 +00:00
New separate project for contracts. Canonicalization.
This commit is contained in:
parent
8bd54dfc74
commit
cbfcac994a
69
build.gradle
69
build.gradle
@ -1,3 +1,12 @@
|
||||
import com.google.common.io.ByteStreams
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.StandardCopyOption
|
||||
import java.nio.file.attribute.FileTime
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
group 'com.r3cev.prototyping'
|
||||
version '1.0-SNAPSHOT'
|
||||
|
||||
@ -26,7 +35,9 @@ buildscript {
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
|
||||
// Dokka (JavaDoc equivalent for Kotlin) download is huge so just comment this out for now.
|
||||
// classpath "org.jetbrains.dokka:dokka-gradle-plugin:0.9.6"
|
||||
//classpath "org.jetbrains.dokka:dokka-gradle-plugin:0.9.6"
|
||||
|
||||
classpath "com.google.guava:guava:19.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,3 +128,59 @@ task runDemoSeller(type: JavaExec, dependsOn: ':classes') {
|
||||
args = ['--dir=seller', '--fake-trade-with=localhost', '--network-address=localhost:31338',
|
||||
'--timestamper-identity-file=buyer/identity-public', '--timestamper-address=localhost']
|
||||
}
|
||||
class CanonicalizerPlugin implements Plugin<Project> {
|
||||
void apply(Project project) {
|
||||
|
||||
project.getTasks().getByName('jar').doLast() {
|
||||
|
||||
def zipPath = (String) project.jar.archivePath
|
||||
def destPath = Files.createTempFile("processzip", null)
|
||||
|
||||
def zeroTime = FileTime.fromMillis(0)
|
||||
|
||||
def input = new ZipFile(zipPath)
|
||||
def entries = input.entries()
|
||||
|
||||
def output = new ZipOutputStream(new FileOutputStream(destPath.toFile()))
|
||||
output.setMethod(ZipOutputStream.STORED)
|
||||
|
||||
while (entries.hasMoreElements()) {
|
||||
def entry = entries.nextElement()
|
||||
|
||||
def newEntry = new ZipEntry( entry.name )
|
||||
|
||||
newEntry.setLastModifiedTime(zeroTime)
|
||||
newEntry.setCreationTime(zeroTime)
|
||||
newEntry.compressedSize = -1
|
||||
newEntry.size = entry.size
|
||||
newEntry.crc = entry.crc
|
||||
|
||||
output.putNextEntry(newEntry)
|
||||
|
||||
ByteStreams.copy(input.getInputStream(entry), output)
|
||||
|
||||
output.closeEntry()
|
||||
}
|
||||
output.close()
|
||||
input.close()
|
||||
|
||||
Files.move(destPath, Paths.get(zipPath), StandardCopyOption.REPLACE_EXISTING)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
project(':contracts') {
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'kotlin'
|
||||
|
||||
apply plugin: CanonicalizerPlugin
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile rootProject
|
||||
}
|
||||
}
|
||||
|
160
contracts/src/main/kotlin/contracts/CommercialPaper.kt
Normal file
160
contracts/src/main/kotlin/contracts/CommercialPaper.kt
Normal file
@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Copyright 2015 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members
|
||||
* pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms
|
||||
* set forth therein.
|
||||
*
|
||||
* All other rights reserved.
|
||||
*/
|
||||
|
||||
package contracts
|
||||
|
||||
import core.*
|
||||
import core.crypto.NullPublicKey
|
||||
import core.crypto.SecureHash
|
||||
import core.crypto.toStringShort
|
||||
import core.utilities.Emoji
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* This is an ultra-trivial implementation of commercial paper, which is essentially a simpler version of a corporate
|
||||
* bond. It can be seen as a company-specific currency. A company issues CP with a particular face value, say $100,
|
||||
* but sells it for less, say $90. The paper can be redeemed for cash at a given date in the future. Thus this example
|
||||
* would have a 10% interest rate with a single repayment. Commercial paper is often rolled over (the maturity date
|
||||
* is adjusted as if the paper was redeemed and immediately repurchased, but without having to front the cash).
|
||||
*
|
||||
* This contract is not intended to realistically model CP. It is here only to act as a next step up above cash in
|
||||
* the prototyping phase. It is thus very incomplete.
|
||||
*
|
||||
* Open issues:
|
||||
* - In this model, you cannot merge or split CP. Can you do this normally? We could model CP as a specialised form
|
||||
* of cash, or reuse some of the cash code? Waiting on response from Ayoub and Rajar about whether CP can always
|
||||
* be split/merged or only in secondary markets. Even if current systems can't do this, would it be a desirable
|
||||
* feature to have anyway?
|
||||
* - The funding steps of CP is totally ignored in this model.
|
||||
* - No attention is paid to the existing roles of custodians, funding banks, etc.
|
||||
* - There are regional variations on the CP concept, for instance, American CP requires a special "CUSIP number"
|
||||
* which may need to be tracked. That, in turn, requires validation logic (there is a bean validator that knows how
|
||||
* to do this in the Apache BVal project).
|
||||
*/
|
||||
|
||||
val CP_PROGRAM_ID = SecureHash.sha256("replace-me-later-with-bytecode-hash")
|
||||
|
||||
// TODO: Generalise the notion of an owned instrument into a superclass/supercontract. Consider composition vs inheritance.
|
||||
class CommercialPaper : Contract {
|
||||
// TODO: should reference the content of the legal agreement, not its URI
|
||||
override val legalContractReference: SecureHash = SecureHash.sha256("https://en.wikipedia.org/wiki/Commercial_paper")
|
||||
|
||||
data class State(
|
||||
val issuance: PartyReference,
|
||||
override val owner: PublicKey,
|
||||
val faceValue: Amount,
|
||||
val maturityDate: Instant
|
||||
) : OwnableState, ICommercialPaperState {
|
||||
override val programRef = CP_PROGRAM_ID
|
||||
|
||||
fun withoutOwner() = copy(owner = NullPublicKey)
|
||||
override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner))
|
||||
override fun toString() = "${Emoji.newspaper}CommercialPaper(of $faceValue redeemable on $maturityDate by '$issuance', owned by ${owner.toStringShort()})"
|
||||
|
||||
// Although kotlin is smart enough not to need these, as we are using the ICommercialPaperState, we need to declare them explicitly for use later,
|
||||
override fun withOwner(newOwner: PublicKey): ICommercialPaperState = copy(owner = newOwner)
|
||||
override fun withIssuance(newIssuance: PartyReference): ICommercialPaperState = copy(issuance = newIssuance)
|
||||
override fun withFaceValue(newFaceValue: Amount): ICommercialPaperState = copy(faceValue = newFaceValue)
|
||||
override fun withMaturityDate(newMaturityDate: Instant): ICommercialPaperState = copy(maturityDate = newMaturityDate)
|
||||
}
|
||||
|
||||
interface Commands : CommandData {
|
||||
class Move : TypeOnlyCommandData(), Commands
|
||||
class Redeem : TypeOnlyCommandData(), Commands
|
||||
// We don't need a nonce in the issue command, because the issuance.reference field should already be unique per CP.
|
||||
// However, nothing in the platform enforces that uniqueness: it's up to the issuer.
|
||||
class Issue : TypeOnlyCommandData(), Commands
|
||||
}
|
||||
|
||||
override fun verify(tx: TransactionForVerification) {
|
||||
// Group by everything except owner: any modification to the CP at all is considered changing it fundamentally.
|
||||
val groups = tx.groupStates<State>() { it.withoutOwner() }
|
||||
|
||||
// There are two possible things that can be done with this CP. The first is trading it. The second is redeeming
|
||||
// it for cash on or after the maturity date.
|
||||
val command = tx.commands.requireSingleCommand<CommercialPaper.Commands>()
|
||||
val timestamp: TimestampCommand? = tx.getTimestampBy(DummyTimestampingAuthority.identity)
|
||||
|
||||
for (group in groups) {
|
||||
when (command.value) {
|
||||
is Commands.Move -> {
|
||||
val input = group.inputs.single()
|
||||
requireThat {
|
||||
"the transaction is signed by the owner of the CP" by (command.signers.contains(input.owner))
|
||||
"the state is propagated" by (group.outputs.size == 1)
|
||||
}
|
||||
}
|
||||
|
||||
is Commands.Redeem -> {
|
||||
val input = group.inputs.single()
|
||||
val received = tx.outStates.sumCashBy(input.owner)
|
||||
val time = timestamp?.after ?: throw IllegalArgumentException("Redemptions must be timestamped")
|
||||
requireThat {
|
||||
"the paper must have matured" by (time > input.maturityDate)
|
||||
"the received amount equals the face value" by (received == input.faceValue)
|
||||
"the paper must be destroyed" by group.outputs.isEmpty()
|
||||
"the transaction is signed by the owner of the CP" by (command.signers.contains(input.owner))
|
||||
}
|
||||
}
|
||||
|
||||
is Commands.Issue -> {
|
||||
val output = group.outputs.single()
|
||||
val time = timestamp?.before ?: throw IllegalArgumentException("Issuances must be timestamped")
|
||||
requireThat {
|
||||
// Don't allow people to issue commercial paper under other entities identities.
|
||||
"the issuance is signed by the claimed issuer of the paper" by
|
||||
(command.signers.contains(output.issuance.party.owningKey))
|
||||
"the face value is not zero" by (output.faceValue.pennies > 0)
|
||||
"the maturity date is not in the past" by (time < output.maturityDate)
|
||||
// Don't allow an existing CP state to be replaced by this issuance.
|
||||
"there is no input state" by group.inputs.isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Think about how to evolve contracts over time with new commands.
|
||||
else -> throw IllegalArgumentException("Unrecognised command")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: PartyReference, faceValue: Amount, maturityDate: Instant): TransactionBuilder {
|
||||
val state = State(issuance, issuance.party.owningKey, faceValue, maturityDate)
|
||||
return TransactionBuilder().withItems(state, 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: PublicKey) {
|
||||
tx.addInputState(paper.ref)
|
||||
tx.addOutputState(paper.state.copy(owner = newOwner))
|
||||
tx.addCommand(Commands.Move(), paper.state.owner)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 wallet doesn't contain enough money to pay the redeemer
|
||||
*/
|
||||
@Throws(InsufficientBalanceException::class)
|
||||
fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef<State>, wallet: List<StateAndRef<Cash.State>>) {
|
||||
// Add the cash movement using the states in our wallet.
|
||||
Cash().generateSpend(tx, paper.state.faceValue, paper.state.owner, wallet)
|
||||
tx.addInputState(paper.ref)
|
||||
tx.addCommand(CommercialPaper.Commands.Redeem(), paper.state.owner)
|
||||
}
|
||||
}
|
||||
|
@ -1,2 +1,2 @@
|
||||
rootProject.name = 'r3prototyping'
|
||||
|
||||
include 'contracts'
|
Loading…
x
Reference in New Issue
Block a user