Merged in mod-crowdfund-close (pull request #5)

Updated close method
This commit is contained in:
Mike Hearn 2015-12-03 10:20:18 +00:00
commit 0e34779fce
3 changed files with 78 additions and 71 deletions

View File

@ -25,8 +25,8 @@ class CrowdFund : Contract {
data class State(
val owner: PublicKey,
val fundingName: String,
val fundingTarget: Amount,
val campaignName: String,
val campaignTarget: Amount,
val closingTime: Instant,
val closed: Boolean = false,
val pledgeTotal: Amount = 0.DOLLARS,
@ -34,7 +34,7 @@ class CrowdFund : Contract {
val pledges: List<Pledge> = ArrayList()
) : ContractState {
override val programRef = CROWDFUND_PROGRAM_ID
override fun toString() = "Crowdsourcing($fundingTarget sought by $owner by $closingTime)"
override fun toString() = "Crowdsourcing($campaignTarget sought by $owner by $closingTime)"
}
data class Pledge(
@ -45,9 +45,8 @@ class CrowdFund : Contract {
interface Commands : Command {
class Register : TypeOnlyCommand(), Commands
class Fund : TypeOnlyCommand(), Commands
class Funded : TypeOnlyCommand(), Commands
class Unfunded : TypeOnlyCommand(), Commands
class Pledge : TypeOnlyCommand(), Commands
class Close : TypeOnlyCommand(), Commands
}
override fun verify(tx: TransactionForVerification) {
@ -56,54 +55,60 @@ class CrowdFund : Contract {
// 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.time
fun requireThatAttributesUnchanged(inputCrowdFund: CrowdFund.State, outputCrowdFund: CrowdFund.State) {
requireThat {
"the owner hasn't changed" by (outputCrowdFund.owner == inputCrowdFund.owner)
"the funding name has not changed" by (outputCrowdFund.campaignName == inputCrowdFund.campaignName)
"the funding target has not changed" by (outputCrowdFund.campaignTarget == inputCrowdFund.campaignTarget)
"the closing time has not changed" by (outputCrowdFund.closingTime == inputCrowdFund.closingTime)
"the pledged total currency is unchanged" by (outputCrowdFund.pledgeTotal.currency == inputCrowdFund.pledgeTotal.currency)
}
}
when (command.value) {
is Commands.Register -> {
requireThat {
"there is no input state" by tx.inStates.filterIsInstance<CrowdFund.State>().isEmpty()
"the transaction is signed by the owner of the crowdsourcing" by (command.signers.contains(outputCrowdFund.owner))
"the output registration is empty of pledges" by (outputCrowdFund.pledges.isEmpty())
"the output registration has a non-zero target" by (outputCrowdFund.fundingTarget.pennies > 0)
"the output registration has a non-zero target" by (outputCrowdFund.campaignTarget.pennies > 0)
"the output registration has a zero starting pledge total" by (outputCrowdFund.pledgeTotal.pennies == 0)
"the output registration has a zero starting pledge count" by (outputCrowdFund.pledgeCount == 0)
"the output registration has a funding currency" by (outputCrowdFund.pledgeTotal.currency.currencyCode.isNotBlank()) // TODO is this necessary? currency is not nullable
"the output registration has a name" by (outputCrowdFund.fundingName.isNotBlank())
"the output registration has a name" by (outputCrowdFund.campaignName.isNotBlank())
"the output registration has a closing time in the future" by (outputCrowdFund.closingTime > tx.time)
"the output registration has an open state" by (!outputCrowdFund.closed)
}
}
is Commands.Fund -> {
is Commands.Pledge -> {
val inputCrowdFund: CrowdFund.State = tx.inStates.filterIsInstance<CrowdFund.State>().single()
val inputCash: List<Cash.State> = tx.inStates.filterIsInstance<Cash.State>()
val pledge = outputCrowdFund.pledges.last()
val pledgedCash = outputCash.single()
val time = tx.time ?: throw IllegalStateException("Transaction must be timestamped")
if (time == null) throw IllegalArgumentException("Redemption transactions must be timestamped")
requireThatAttributesUnchanged(inputCrowdFund, outputCrowdFund)
requireThat {
"the funding is still open" by (time <= inputCrowdFund.closingTime)
"the campaign is still open" by (inputCrowdFund.closingTime >= time)
// TODO "the transaction is signed by the owner of the pledge" by (command.signers.contains(inputCrowdFund.owner))
"the transaction is signed by the pledge-maker" by (command.signers.contains(pledge.owner))
"the pledge must be for a non-zero amount" by (pledge.amount.pennies > 0)
"the pledge must be in the same currency as the goal" by (pledge.amount.currency == outputCrowdFund.fundingTarget.currency)
"the pledge must be in the same currency as the goal" by (pledge.amount.currency == outputCrowdFund.campaignTarget.currency)
"the number of pledges must have increased by one" by (outputCrowdFund.pledgeCount == inputCrowdFund.pledgeCount + 1)
"the pledged total has increased by the value of the pledge" by (outputCrowdFund.pledgeTotal.pennies == inputCrowdFund.pledgeTotal.pennies + inputCash.sumCash().pennies)
"the pledge has been added to the list of pledges" by (outputCrowdFund.pledges.size == outputCrowdFund.pledgeCount)
"the cash input has been assigned to the funding owner" by (pledgedCash.owner == inputCrowdFund.owner)
// TODO how to simplify the boilerplate associated with unchanged elements
"the owner hasn't changed" by (outputCrowdFund.owner == inputCrowdFund.owner)
"the funding name has not changed" by (outputCrowdFund.fundingName == inputCrowdFund.fundingName)
"the funding target has not changed" by (outputCrowdFund.fundingTarget == inputCrowdFund.fundingTarget)
"the closing time has not changed" by (outputCrowdFund.closingTime == inputCrowdFund.closingTime)
"the pledged total currency is unchanged" by (outputCrowdFund.pledgeTotal.currency == inputCrowdFund.pledgeTotal.currency)
"the cash input has been assigned to the campaign owner" by (pledgedCash.owner == inputCrowdFund.owner)
"the output registration has an open state" by (!outputCrowdFund.closed)
}
}
is Commands.Unfunded -> {
is Commands.Close -> {
val inputCrowdFund: CrowdFund.State = tx.inStates.filterIsInstance<CrowdFund.State>().single()
// TODO how can this be made smarter? feels wrong as a separate function
// TODO include inline rather than as separate function
fun checkReturns(inputCrowdFund: CrowdFund.State, outputCash: List<Cash.State>): Boolean {
for (pledge in inputCrowdFund.pledges) {
if (outputCash.filter { it.amount == pledge.amount && it.owner == pledge.owner }.isEmpty()) return false
@ -111,75 +116,61 @@ class CrowdFund : Contract {
return true
}
val time = tx.time ?: throw IllegalStateException("Transaction must be timestamped")
if (time == null) throw IllegalArgumentException("Redemption transactions must be timestamped")
requireThatAttributesUnchanged(inputCrowdFund, outputCrowdFund)
requireThat {
"the closing date has past" by (time >= outputCrowdFund.closingTime)
"the pledges did not meet the target" by (inputCrowdFund.pledgeTotal < inputCrowdFund.fundingTarget)
"the output cash returns equal the pledge total, if the target is not reached" by (outputCash.sumCash() == inputCrowdFund.pledgeTotal)
"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 input has an open state" by (!inputCrowdFund.closed)
"the output registration has a closed state" by (outputCrowdFund.closed)
// TODO how to simplify the boilerplate associated with unchanged elements
"the owner hasn't changed" by (outputCrowdFund.owner == inputCrowdFund.owner)
"the funding name has not changed" by (outputCrowdFund.fundingName == inputCrowdFund.fundingName)
"the funding target has not changed" by (outputCrowdFund.fundingTarget == inputCrowdFund.fundingTarget)
"the closing time has not changed" by (outputCrowdFund.closingTime == inputCrowdFund.closingTime)
"the pledged total is unchanged" by (outputCrowdFund.pledgeTotal == inputCrowdFund.pledgeTotal)
"the pledged count is unchanged" by (outputCrowdFund.pledgeCount == inputCrowdFund.pledgeCount)
"the pledges are unchanged" by (outputCrowdFund.pledges == inputCrowdFund.pledges)
}
}
is Commands.Funded -> {
val inputCrowdFund: CrowdFund.State = tx.inStates.filterIsInstance<CrowdFund.State>().single()
val time = tx.time ?: throw IllegalStateException("Transaction must be timestamped")
requireThat {
"the closing date has past" by (time >= outputCrowdFund.closingTime)
"the input has an open state" by (!inputCrowdFund.closed)
"the output registration has a closed state" by (outputCrowdFund.closed)
// TODO how to simplify the boilerplate associated with unchanged elements
"the owner hasn't changed" by (outputCrowdFund.owner == inputCrowdFund.owner)
"the funding name has not changed" by (outputCrowdFund.fundingName == inputCrowdFund.fundingName)
"the funding target has not changed" by (outputCrowdFund.fundingTarget == inputCrowdFund.fundingTarget)
"the closing time has not changed" by (outputCrowdFund.closingTime == inputCrowdFund.closingTime)
// Now check whether the target was not, and if so, return cash
if (inputCrowdFund.pledgeTotal < inputCrowdFund.campaignTarget) {
"the output cash returns equal the pledge total, if the target is not reached" by (outputCash.sumCash() == inputCrowdFund.pledgeTotal)
"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.pledgeTotal == inputCrowdFund.pledgeTotal)
"the pledged count is unchanged" by (outputCrowdFund.pledgeCount == inputCrowdFund.pledgeCount)
"the pledges are unchanged" by (outputCrowdFund.pledges == inputCrowdFund.pledges)
}
}
// TODO: Think about how to evolve contracts over time with new commands.
else -> throw IllegalArgumentException("Unrecognised command")
}
}
/**
* Returns a transaction that registers a crowd-funding campaing, owned by the issuing parties key. Does not update
* 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 craftRegister(owner: PartyReference, fundingTarget: Amount, fundingName: String, closingTime: Instant): PartialTransaction {
val state = State(owner = owner.party.owningKey, fundingName = fundingName, fundingTarget = fundingTarget, closingTime = closingTime)
return PartialTransaction(state, WireCommand(CrowdFund.Commands.Register(), owner.party.owningKey))
val state = State(owner = owner.party.owningKey, campaignName = fundingName, campaignTarget = fundingTarget, closingTime = closingTime)
return PartialTransaction(state, WireCommand(Commands.Register(), owner.party.owningKey))
}
/**
* Updates the given partial transaction with an input/output/command to fund the opportunity.
*/
fun craftFund(tx: PartialTransaction, campaign: StateAndRef<State>, subscriber: PublicKey) {
fun craftPledge(tx: PartialTransaction, campaign: StateAndRef<State>, subscriber: PublicKey) {
tx.addInputState(campaign.ref)
tx.addOutputState(campaign.state.copy(
pledges = campaign.state.pledges + CrowdFund.Pledge(subscriber, 1000.DOLLARS),
pledgeCount = campaign.state.pledgeCount + 1,
pledgeTotal = campaign.state.pledgeTotal + 1000.DOLLARS
))
tx.addArg(WireCommand(CrowdFund.Commands.Fund(), subscriber))
tx.addArg(WireCommand(Commands.Pledge(), subscriber))
}
fun craftFunded(tx: PartialTransaction, campaign: StateAndRef<State>) {
fun craftClose(tx: PartialTransaction, campaign: StateAndRef<State>, wallet: List<StateAndRef<Cash.State>>) {
tx.addInputState(campaign.ref)
tx.addOutputState(campaign.state.copy(closed = true))
tx.addArg(WireCommand(CrowdFund.Commands.Funded(), campaign.state.owner))
tx.addArg(WireCommand(Commands.Close(), campaign.state.owner))
// If campaign target has not been met, compose cash returns
if (campaign.state.pledgeTotal < campaign.state.campaignTarget) {
for (pledge in campaign.state.pledges) {
Cash().craftSpend(tx, pledge.amount, pledge.owner, wallet)
}
}
}
override val legalContractReference: SecureHash = SecureHash.sha256("Crowdsourcing")

View File

@ -264,8 +264,8 @@ fun createKryo(): Kryo {
registerDataClass<CrowdFund.State>()
registerDataClass<CrowdFund.Pledge>()
register(CrowdFund.Commands.Register::class.java)
register(CrowdFund.Commands.Fund::class.java)
register(CrowdFund.Commands.Funded::class.java)
register(CrowdFund.Commands.Pledge::class.java)
register(CrowdFund.Commands.Close::class.java)
// And for unit testing ...
registerDataClass<DummyPublicKey>()

View File

@ -11,13 +11,16 @@ package contracts
import core.*
import core.testutils.*
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(
owner = MINI_CORP_PUBKEY,
fundingName = "kickstart me",
fundingTarget = 1000.DOLLARS,
campaignName = "kickstart me",
campaignTarget = 1000.DOLLARS,
pledgeTotal = 0.DOLLARS,
pledgeCount = 0,
closingTime = TEST_TX_TIME + 7.days,
@ -79,14 +82,14 @@ class CrowdFundTests {
}
output { 1000.DOLLARS.CASH `owned by` MINI_CORP_PUBKEY }
arg(ALICE) { Cash.Commands.Move() }
arg(ALICE) { CrowdFund.Commands.Fund() }
arg(ALICE) { CrowdFund.Commands.Pledge() }
}
// 3. Close the opportunity, assuming the target has been met
transaction(time = TEST_TX_TIME + 8.days) {
input ("pledged opportunity")
output ("funded and closed") { "pledged opportunity".output.copy(closed = true) }
arg(MINI_CORP_PUBKEY) { CrowdFund.Commands.Funded() }
arg(MINI_CORP_PUBKEY) { CrowdFund.Commands.Close() }
}
}
}
@ -97,8 +100,8 @@ class CrowdFundTests {
}
@Test
fun `raise more funds`() {
// MiniCorp registers a crowdfunding of $1,000, to close in 30 days.
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().craftRegister(MINI_CORP.ref(123), 1000.DOLLARS, "crowd funding", TEST_TX_TIME + 7.days)
@ -117,7 +120,7 @@ class CrowdFundTests {
// Alice pays $1000 to MiniCorp to fund their campaign.
val pledgeTX: LedgerTransaction = run {
val ptx = PartialTransaction()
CrowdFund().craftFund(ptx, registerTX.outRef(0), ALICE)
CrowdFund().craftPledge(ptx, registerTX.outRef(0), ALICE)
Cash().craftSpend(ptx, 1000.DOLLARS, MINI_CORP_PUBKEY, aliceWallet)
ptx.signWith(ALICE_KEY)
val stx = ptx.toSignedTransaction()
@ -125,17 +128,30 @@ class CrowdFundTests {
stx.verify().toLedgerTransaction(TEST_TX_TIME, TEST_KEYS_TO_CORP_MAP, SecureHash.randomSHA256())
}
// 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.
val fundedTX: LedgerTransaction = run {
fun makeFundedTX(time: Instant): LedgerTransaction {
val ptx = PartialTransaction()
CrowdFund().craftFunded(ptx, pledgeTX.outRef(0))
CrowdFund().craftClose(ptx, pledgeTX.outRef(0), miniCorpWallet)
ptx.signWith(MINI_CORP_KEY)
val stx = ptx.toSignedTransaction()
stx.verify().toLedgerTransaction(TEST_TX_TIME + 8.days, TEST_KEYS_TO_CORP_MAP, SecureHash.randomSHA256())
return stx.verify().toLedgerTransaction(time, TEST_KEYS_TO_CORP_MAP, SecureHash.randomSHA256())
}
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(TEST_PROGRAM_MAP)
}
assertTrue(e.cause!!.message!!.contains("the closing date has past"))
// This verification passes
TransactionGroup(setOf(registerTX, pledgeTX, fundedTX), setOf(aliceWalletTX)).verify(TEST_PROGRAM_MAP)
TransactionGroup(setOf(registerTX, pledgeTX, validClose), setOf(aliceWalletTX)).verify(TEST_PROGRAM_MAP)
}