Merged in rnicoll-cash-generate-exit (pull request #233)

Add Cash.generateExit() function
This commit is contained in:
Ross Nicoll 2016-07-20 13:21:29 +01:00
commit f866c4689e
2 changed files with 129 additions and 13 deletions

View File

@ -125,6 +125,75 @@ class Cash : ClauseVerifier() {
data class Exit(override val amount: Amount<Issued<Currency>>) : Commands, FungibleAsset.Commands.Exit<Currency> data class Exit(override val amount: Amount<Issued<Currency>>) : Commands, FungibleAsset.Commands.Exit<Currency>
} }
/**
* Generate an transaction exiting cash 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 changeKey the key to send any change to. This needs to be explicitly stated as the input states are not
* necessarily owned by us.
* @param cashStates the cash 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 key of the cash issuer, who must sign the transaction for it to be valid.
*/
// TODO: I'm not at all sure we should support exiting funds others hold, but that's a bigger discussion around the
// contract logic, not just this function.
fun generateExit(tx: TransactionBuilder, amountIssued: Amount<Issued<Currency>>,
changeKey: PublicKey, cashStates: List<StateAndRef<State>>): PublicKey {
val currency = amountIssued.token.product
val issuer = amountIssued.token.issuer.party
val amount = Amount(amountIssued.quantity, currency)
var acceptableCoins = cashStates.filter { ref -> ref.state.data.amount.token == amountIssued.token }
val notary = acceptableCoins.firstOrNull()?.state?.notary
// TODO: We should be prepared to produce multiple transactions exiting inputs from
// different notaries, or at least group states by notary and take the set with the
// highest total value
acceptableCoins = acceptableCoins.filter { it.state.notary == notary }
val (gathered, gatheredAmount) = gatherCoins(acceptableCoins, Amount(amount.quantity, currency))
val takeChangeFrom = gathered.lastOrNull()
val change = if (takeChangeFrom != null && gatheredAmount > amount) {
Amount<Issued<Currency>>(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.data.issuanceDef)
} else {
null
}
val outputs: List<TransactionState<Cash.State>> = if (change != null) {
// Add a change output and adjust the last output downwards.
listOf(TransactionState(State(amountIssued.token.issuer, Amount(change.quantity, currency), changeKey),
notary!!))
} else emptyList()
for (state in gathered) tx.addInputState(state)
for (state in outputs) tx.addOutputState(state)
tx.addCommand(Commands.Exit(amountIssued), amountIssued.token.issuer.party.owningKey)
return amountIssued.token.issuer.party.owningKey
}
/**
* Gather coins from the given list of states, sufficient to match or exceed the given amount.
*
* @param acceptableCoins list of states to use as inputs.
* @param amount the amount to gather states up to.
* @throws InsufficientBalanceException if there isn't enough value in the states to cover the requested amount.
*/
@Throws(InsufficientBalanceException::class)
private fun gatherCoins(acceptableCoins: List<StateAndRef<State>>,
amount: Amount<Currency>): Pair<ArrayList<StateAndRef<State>>, Amount<Currency>> {
val gathered = arrayListOf<StateAndRef<State>>()
var gatheredAmount = Amount(0, amount.token)
for (c in acceptableCoins) {
if (gatheredAmount >= amount) break
gathered.add(c)
gatheredAmount += Amount(c.state.data.amount.quantity, amount.token)
}
if (gatheredAmount < amount)
throw InsufficientBalanceException(amount - gatheredAmount)
return Pair(gathered, gatheredAmount)
}
/** /**
* Puts together an issuance transaction from the given template, that starts out being owned by the given pubkey. * Puts together an issuance transaction from the given template, that starts out being owned by the given pubkey.
*/ */
@ -192,19 +261,8 @@ class Cash : ClauseVerifier() {
ofCurrency ofCurrency
} }
val gathered = arrayListOf<StateAndRef<State>>() val (gathered, gatheredAmount) = gatherCoins(acceptableCoins, amount)
var gatheredAmount = Amount(0, currency) val takeChangeFrom = gathered.firstOrNull()
var takeChangeFrom: StateAndRef<State>? = null
for (c in acceptableCoins) {
if (gatheredAmount >= amount) break
gathered.add(c)
gatheredAmount += Amount(c.state.data.amount.quantity, currency)
takeChangeFrom = c
}
if (gatheredAmount < amount)
throw InsufficientBalanceException(amount - gatheredAmount)
val change = if (takeChangeFrom != null && gatheredAmount > amount) { val change = if (takeChangeFrom != null && gatheredAmount > amount) {
Amount<Issued<Currency>>(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.data.issuanceDef) Amount<Issued<Currency>>(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.data.issuanceDef)
} else { } else {

View File

@ -380,12 +380,70 @@ class CashTests {
makeCash(80.SWISS_FRANCS, MINI_CORP, 2) makeCash(80.SWISS_FRANCS, MINI_CORP, 2)
) )
/**
* Generate an exit transaction, removing some amount of cash from the ledger.
*/
fun makeExit(amount: Amount<Currency>, corp: Party, depositRef: Byte = 1): WireTransaction {
val tx = TransactionType.General.Builder()
Cash().generateExit(tx, Amount(amount.quantity, Issued(corp.ref(depositRef), amount.token)), OUR_PUBKEY_1, WALLET)
return tx.toWireTransaction()
}
fun makeSpend(amount: Amount<Currency>, dest: PublicKey): WireTransaction { fun makeSpend(amount: Amount<Currency>, dest: PublicKey): WireTransaction {
val tx = TransactionType.General.Builder() val tx = TransactionType.General.Builder()
Cash().generateSpend(tx, amount, dest, WALLET) Cash().generateSpend(tx, amount, dest, WALLET)
return tx.toWireTransaction() return tx.toWireTransaction()
} }
/**
* Try exiting an amount which matches a single state.
*/
@Test
fun generateSimpleExit() {
val wtx = makeExit(100.DOLLARS, MEGA_CORP, 1)
assertEquals(WALLET[0].ref, wtx.inputs[0])
assertEquals(0, wtx.outputs.size)
val expected = Cash.Commands.Exit(Amount(10000, Issued(MEGA_CORP.ref(1), USD)))
val actual = wtx.commands.single().value
assertEquals(expected, actual)
}
/**
* Try exiting an amount smaller than the smallest available input state, and confirm change is generated correctly.
*/
@Test
fun generatePartialExit() {
val wtx = makeExit(50.DOLLARS, MEGA_CORP, 1)
assertEquals(WALLET[0].ref, wtx.inputs[0])
assertEquals(1, wtx.outputs.size)
assertEquals(WALLET[0].state.data.copy(amount = WALLET[0].state.data.amount / 2), wtx.outputs[0].data)
}
/**
* Try exiting a currency we don't have.
*/
@Test
fun generateAbsentExit() {
assertFailsWith<InsufficientBalanceException> { makeExit(100.POUNDS, MEGA_CORP, 1) }
}
/**
* Try exiting with a reference mis-match.
*/
@Test
fun generateInvalidReferenceExit() {
assertFailsWith<InsufficientBalanceException> { makeExit(100.POUNDS, MEGA_CORP, 2) }
}
/**
* Try exiting an amount greater than the maximum available.
*/
@Test
fun generateInsufficientExit() {
assertFailsWith<InsufficientBalanceException> { makeExit(1000.DOLLARS, MEGA_CORP, 1) }
}
@Test @Test
fun generateSimpleDirectSpend() { fun generateSimpleDirectSpend() {
val wtx = makeSpend(100.DOLLARS, THEIR_PUBKEY_1) val wtx = makeSpend(100.DOLLARS, THEIR_PUBKEY_1)