Merged in removecrowdfundcontract (pull request #147)

Removed the CrowdFund contract since it is written in a style that is not typical of Corda contracts.
This commit is contained in:
Clinton Alexander 2016-06-15 14:53:47 +01:00
commit 8aa6fa156a
3 changed files with 0 additions and 340 deletions

View File

@ -1,174 +0,0 @@
package com.r3corda.contracts
import com.r3corda.contracts.cash.Cash
import com.r3corda.contracts.cash.sumCash
import com.r3corda.contracts.cash.sumCashBy
import com.r3corda.core.*
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import java.security.PublicKey
import java.time.Instant
import java.util.*
val CROWDFUND_PROGRAM_ID = CrowdFund()
/**
* This is a basic crowd funding contract. It allows a party to create a funding opportunity, then for others to
* pledge during the funding period , and then for the party to either accept the funding (if the target has been reached)
* return the funds to the pledge-makers (if the target has not been reached).
*
* Discussion
* ----------
*
* This method of modelling a crowdfund is similar to how it'd be done in Ethereum. The state is essentially a database
* in which transactions evolve it over time. The state transition model we are using here though means it's possible
* to do it in a different approach, with some additional (not yet implemented) extensions to the model. In the UTXO
* model you can do something more like the Lighthouse application (https://www.vinumeris.com/lighthouse) in which
* the campaign data and people's pledges are transmitted out of band, with a pledge being a partially signed
* transaction which is valid only when merged with other transactions. The pledges can then be combined by the project
* owner at the point at which sufficient amounts of money have been gathered, and this creates a valid transaction
* that claims the money.
*
* TODO: Prototype this second variant of crowdfunding once the core model has been sufficiently extended.
* TODO: Experiment with the use of the javax.validation API to simplify the validation logic by annotating state members.
*
* See JIRA bug PD-21 for further discussion and followup.
*
* @author James Carlyle
*/
class CrowdFund : Contract {
data class Campaign(
val owner: PublicKey,
val name: String,
val target: Amount<Currency>,
val closingTime: Instant
) {
override fun toString() = "Crowdsourcing($target sought by $owner by $closingTime)"
}
data class State(
val campaign: Campaign,
override val notary: Party,
val closed: Boolean = false,
val pledges: List<Pledge> = ArrayList()
) : ContractState {
override val contract = CROWDFUND_PROGRAM_ID
val pledgedAmount: Amount<Currency> get() = pledges.map { it.amount }.sumOrZero(campaign.target.token)
}
data class Pledge(
val owner: PublicKey,
val amount: Amount<Currency>
)
interface Commands : CommandData {
class Register : TypeOnlyCommandData(), Commands
class Pledge : TypeOnlyCommandData(), Commands
class Close : TypeOnlyCommandData(), Commands
}
override fun verify(tx: TransactionForVerification) {
// There are three possible things that can be done with Crowdsourcing.
// The first is creating it. The second is funding it with cash. The third is closing it on or after the closing
// date, and returning funds to pledge-makers if the target is unmet, or passing to the recipient.
val command = tx.commands.requireSingleCommand<CrowdFund.Commands>()
val outputCrowdFund: CrowdFund.State = tx.outStates.filterIsInstance<CrowdFund.State>().single()
val outputCash: List<Cash.State> = tx.outStates.filterIsInstance<Cash.State>()
val time = tx.commands.getTimestampByName("Notary Service")?.midpoint
if (time == null) throw IllegalArgumentException("must be timestamped")
when (command.value) {
is Commands.Register -> {
requireThat {
"there is no input state" by tx.inStates.filterIsInstance<State>().isEmpty()
"the transaction is signed by the owner of the crowdsourcing" by (command.signers.contains(outputCrowdFund.campaign.owner))
"the output registration is empty of pledges" by (outputCrowdFund.pledges.isEmpty())
"the output registration has a non-zero target" by (outputCrowdFund.campaign.target.quantity > 0)
"the output registration has a name" by (outputCrowdFund.campaign.name.isNotBlank())
"the output registration has a closing time in the future" by (time < outputCrowdFund.campaign.closingTime)
"the output registration has an open state" by (!outputCrowdFund.closed)
}
}
is Commands.Pledge -> {
val inputCrowdFund: CrowdFund.State = tx.inStates.filterIsInstance<CrowdFund.State>().single()
val pledgedCash = outputCash.sumCashBy(inputCrowdFund.campaign.owner)
requireThat {
"campaign details have not changed" by (inputCrowdFund.campaign == outputCrowdFund.campaign)
"the campaign is still open" by (inputCrowdFund.campaign.closingTime >= time)
"the pledge must be in the same currency as the goal" by (pledgedCash.token == outputCrowdFund.campaign.target.token)
"the pledged total has increased by the value of the pledge" by (outputCrowdFund.pledgedAmount == inputCrowdFund.pledgedAmount + pledgedCash)
"the output registration has an open state" by (!outputCrowdFund.closed)
}
}
is Commands.Close -> {
val inputCrowdFund: CrowdFund.State = tx.inStates.filterIsInstance<CrowdFund.State>().single()
fun checkReturns(inputCrowdFund: CrowdFund.State, outputCash: List<Cash.State>): Boolean {
for (pledge in inputCrowdFund.pledges) {
if (outputCash.none { it.amount == pledge.amount && it.owner == pledge.owner }) return false
}
return true
}
requireThat {
"campaign details have not changed" by (inputCrowdFund.campaign == outputCrowdFund.campaign)
"the closing date has past" by (time >= outputCrowdFund.campaign.closingTime)
"the input has an open state" by (!inputCrowdFund.closed)
"the output registration has a closed state" by (outputCrowdFund.closed)
// Now check whether the target was met, and if so, return cash
if (inputCrowdFund.pledgedAmount < inputCrowdFund.campaign.target) {
"the output cash returns equal the pledge total, if the target is not reached" by (outputCash.sumCash() == inputCrowdFund.pledgedAmount)
"the output cash is distributed to the pledge-makers, if the target is not reached" by (checkReturns(inputCrowdFund, outputCash))
"the output cash is distributed to the pledge-makers, if the target is not reached" by (outputCash.map { it.amount }.containsAll(inputCrowdFund.pledges.map { it.amount }))
}
"the pledged total is unchanged" by (outputCrowdFund.pledgedAmount == inputCrowdFund.pledgedAmount)
"the pledges are unchanged" by (outputCrowdFund.pledges == inputCrowdFund.pledges)
}
}
else -> throw IllegalArgumentException("Unrecognised command")
}
}
/**
* Returns a transaction that registers a crowd-funding campaing, owned by the issuing institution's key. Does not update
* an existing transaction because it's not possible to register multiple campaigns in a single transaction
*/
fun generateRegister(owner: PartyAndReference, fundingTarget: Amount<Currency>, fundingName: String, closingTime: Instant, notary: Party): TransactionBuilder {
val campaign = Campaign(owner = owner.party.owningKey, name = fundingName, target = fundingTarget, closingTime = closingTime)
val state = State(campaign, notary)
return TransactionBuilder().withItems(state, Command(Commands.Register(), owner.party.owningKey))
}
/**
* Updates the given partial transaction with an input/output/command to fund the opportunity.
*/
fun generatePledge(tx: TransactionBuilder, campaign: StateAndRef<State>, subscriber: PublicKey) {
tx.addInputState(campaign.ref)
tx.addOutputState(campaign.state.copy(
pledges = campaign.state.pledges + CrowdFund.Pledge(subscriber, 1000.DOLLARS)
))
tx.addCommand(Commands.Pledge(), subscriber)
}
fun generateClose(tx: TransactionBuilder, campaign: StateAndRef<State>, wallet: List<StateAndRef<Cash.State>>) {
tx.addInputState(campaign.ref)
tx.addOutputState(campaign.state.copy(closed = true))
tx.addCommand(Commands.Close(), campaign.state.campaign.owner)
// If campaign target has not been met, compose cash returns
if (campaign.state.pledgedAmount < campaign.state.campaign.target) {
for (pledge in campaign.state.pledges) {
Cash().generateSpend(tx, pledge.amount, pledge.owner, wallet)
}
}
}
override val legalContractReference: SecureHash = SecureHash.sha256("Crowdsourcing")
}

View File

@ -18,7 +18,6 @@ val TEST_PROGRAM_MAP: Map<Contract, Class<out Contract>> = mapOf(
CASH_PROGRAM_ID to Cash::class.java,
CP_PROGRAM_ID to CommercialPaper::class.java,
JavaCommercialPaper.JCP_PROGRAM_ID to JavaCommercialPaper::class.java,
CROWDFUND_PROGRAM_ID to CrowdFund::class.java,
DUMMY_PROGRAM_ID to DummyContract::class.java,
IRS_PROGRAM_ID to InterestRateSwap::class.java
)

View File

@ -1,165 +0,0 @@
package com.r3corda.contracts
import com.r3corda.contracts.cash.Cash
import com.r3corda.contracts.testing.CASH
import com.r3corda.contracts.testing.`owned by`
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.days
import com.r3corda.core.node.services.testing.MockStorageService
import com.r3corda.core.seconds
import com.r3corda.core.testing.*
import org.junit.Test
import java.time.Instant
import java.util.*
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
class CrowdFundTests {
val CF_1 = CrowdFund.State(
campaign = CrowdFund.Campaign(
owner = MINI_CORP_PUBKEY,
name = "kickstart me",
target = 1000.DOLLARS,
closingTime = TEST_TX_TIME + 7.days
),
closed = false,
pledges = ArrayList<CrowdFund.Pledge>(),
notary = DUMMY_NOTARY
)
val attachments = MockStorageService().attachments
@Test
fun `key mismatch at issue`() {
transactionGroup {
transaction {
output { CF_1 }
arg(DUMMY_PUBKEY_1) { CrowdFund.Commands.Register() }
timestamp(TEST_TX_TIME)
}
expectFailureOfTx(1, "the transaction is signed by the owner of the crowdsourcing")
}
}
@Test
fun `closing time not in the future`() {
transactionGroup {
transaction {
output { CF_1.copy(campaign = CF_1.campaign.copy(closingTime = TEST_TX_TIME - 1.days)) }
arg(MINI_CORP_PUBKEY) { CrowdFund.Commands.Register() }
timestamp(TEST_TX_TIME)
}
expectFailureOfTx(1, "the output registration has a closing time in the future")
}
}
@Test
fun ok() {
raiseFunds().verify()
}
private fun raiseFunds(): TransactionGroupDSL<CrowdFund.State> {
return transactionGroupFor {
roots {
transaction(1000.DOLLARS.CASH `owned by` ALICE_PUBKEY label "alice's $1000")
}
// 1. Create the funding opportunity
transaction {
output("funding opportunity") { CF_1 }
arg(MINI_CORP_PUBKEY) { CrowdFund.Commands.Register() }
timestamp(TEST_TX_TIME)
}
// 2. Place a pledge
transaction {
input ("funding opportunity")
input("alice's $1000")
output ("pledged opportunity") {
CF_1.copy(
pledges = CF_1.pledges + CrowdFund.Pledge(ALICE_PUBKEY, 1000.DOLLARS)
)
}
output { 1000.DOLLARS.CASH `owned by` MINI_CORP_PUBKEY }
arg(ALICE_PUBKEY) { Cash.Commands.Move() }
arg(ALICE_PUBKEY) { CrowdFund.Commands.Pledge() }
timestamp(TEST_TX_TIME)
}
// 3. Close the opportunity, assuming the target has been met
transaction {
input ("pledged opportunity")
output ("funded and closed") { "pledged opportunity".output.copy(closed = true) }
arg(MINI_CORP_PUBKEY) { CrowdFund.Commands.Close() }
timestamp(time = TEST_TX_TIME + 8.days)
}
}
}
fun cashOutputsToWallet(vararg states: Cash.State): Pair<LedgerTransaction, List<StateAndRef<Cash.State>>> {
val ltx = LedgerTransaction(emptyList(), emptyList(), listOf(*states), emptyList(), SecureHash.randomSHA256())
return Pair(ltx, states.mapIndexed { index, state -> StateAndRef(state, StateRef(ltx.id, index)) })
}
@Test
fun `raise more funds using output-state generation functions`() {
// MiniCorp registers a crowdfunding of $1,000, to close in 7 days.
val registerTX: LedgerTransaction = run {
// craftRegister returns a partial transaction
val ptx = CrowdFund().generateRegister(MINI_CORP.ref(123), 1000.DOLLARS, "crowd funding", TEST_TX_TIME + 7.days, DUMMY_NOTARY).apply {
setTime(TEST_TX_TIME, DUMMY_NOTARY, 30.seconds)
signWith(MINI_CORP_KEY)
signWith(DUMMY_NOTARY_KEY)
}
ptx.toSignedTransaction().verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, attachments)
}
// let's give Alice some funds that she can invest
val (aliceWalletTX, aliceWallet) = cashOutputsToWallet(
200.DOLLARS.CASH `owned by` ALICE_PUBKEY,
500.DOLLARS.CASH `owned by` ALICE_PUBKEY,
300.DOLLARS.CASH `owned by` ALICE_PUBKEY
)
// Alice pays $1000 to MiniCorp to fund their campaign.
val pledgeTX: LedgerTransaction = run {
val ptx = TransactionBuilder()
CrowdFund().generatePledge(ptx, registerTX.outRef(0), ALICE_PUBKEY)
Cash().generateSpend(ptx, 1000.DOLLARS, MINI_CORP_PUBKEY, aliceWallet)
ptx.setTime(TEST_TX_TIME, DUMMY_NOTARY, 30.seconds)
ptx.signWith(ALICE_KEY)
ptx.signWith(DUMMY_NOTARY_KEY)
// this verify passes - the transaction contains an output cash, necessary to verify the fund command
ptx.toSignedTransaction().verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, attachments)
}
// Won't be validated.
val (miniCorpWalletTx, miniCorpWallet) = cashOutputsToWallet(
900.DOLLARS.CASH `owned by` MINI_CORP_PUBKEY,
400.DOLLARS.CASH `owned by` MINI_CORP_PUBKEY
)
// MiniCorp closes their campaign.
fun makeFundedTX(time: Instant): LedgerTransaction {
val ptx = TransactionBuilder()
ptx.setTime(time, DUMMY_NOTARY, 30.seconds)
CrowdFund().generateClose(ptx, pledgeTX.outRef(0), miniCorpWallet)
ptx.signWith(MINI_CORP_KEY)
ptx.signWith(DUMMY_NOTARY_KEY)
return ptx.toSignedTransaction().verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, attachments)
}
val tooEarlyClose = makeFundedTX(TEST_TX_TIME + 6.days)
val validClose = makeFundedTX(TEST_TX_TIME + 8.days)
val e = assertFailsWith(TransactionVerificationException::class) {
TransactionGroup(setOf(registerTX, pledgeTX, tooEarlyClose), setOf(miniCorpWalletTx, aliceWalletTX)).verify()
}
assertTrue(e.cause!!.message!!.contains("the closing date has past"))
// This verification passes
TransactionGroup(setOf(registerTX, pledgeTX, validClose), setOf(aliceWalletTX)).verify()
}
}