mirror of
https://github.com/corda/corda.git
synced 2024-12-20 05:28:21 +00:00
Merged in rnicoll-cash-generate-exit (pull request #233)
Add Cash.generateExit() function
This commit is contained in:
commit
f866c4689e
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user