diff --git a/core/src/main/kotlin/net/corda/core/node/services/Services.kt b/core/src/main/kotlin/net/corda/core/node/services/Services.kt index 536e032910..43d16f732b 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/Services.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/Services.kt @@ -6,6 +6,7 @@ import net.corda.core.contracts.* import net.corda.core.crypto.* import net.corda.core.flows.FlowException import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.OpaqueBytes import net.corda.core.toFuture import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder @@ -253,7 +254,7 @@ interface VaultService { * is implemented in a separate module (finance) and requires access to it. */ @Suspendable - fun unconsumedStatesForSpending(amount: Amount, onlyFromIssuerParties: Set? = null, notary: Party? = null, lockId: UUID): List> + fun unconsumedStatesForSpending(amount: Amount, onlyFromIssuerParties: Set? = null, notary: Party? = null, lockId: UUID, withIssuerRefs: Set? = null): List> } inline fun VaultService.unconsumedStates(includeSoftLockedStates: Boolean = true): Iterable> = diff --git a/finance/src/main/kotlin/net/corda/contracts/clause/AbstractConserveAmount.kt b/finance/src/main/kotlin/net/corda/contracts/clause/AbstractConserveAmount.kt index 1863997635..67f0099766 100644 --- a/finance/src/main/kotlin/net/corda/contracts/clause/AbstractConserveAmount.kt +++ b/finance/src/main/kotlin/net/corda/contracts/clause/AbstractConserveAmount.kt @@ -4,6 +4,8 @@ import net.corda.core.contracts.* import net.corda.core.contracts.clauses.Clause import net.corda.core.crypto.CompositeKey import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.trace import java.util.* /** @@ -12,6 +14,11 @@ import java.util.* * errors on no-match, ends on match. */ abstract class AbstractConserveAmount, C : CommandData, T : Any> : Clause>() { + + private companion object { + val log = loggerFor>() + } + /** * Gather assets from the given list of states, sufficient to match or exceed the given amount. * @@ -30,9 +37,12 @@ abstract class AbstractConserveAmount, C : CommandData, T : gatheredAmount += Amount(c.state.data.amount.quantity, amount.token) } - if (gatheredAmount < amount) + if (gatheredAmount < amount) { + log.trace { "Insufficient balance: requested $amount, available $gatheredAmount" } throw InsufficientBalanceException(amount - gatheredAmount) + } + log.trace { "Gathered coins: requested $amount, available $gatheredAmount, change: ${gatheredAmount - amount}" } return Pair(gathered, gatheredAmount) } @@ -61,7 +71,7 @@ abstract class AbstractConserveAmount, C : CommandData, T : // highest total value acceptableCoins = acceptableCoins.filter { it.state.notary == tx.notary } - val (gathered, gatheredAmount) = gatherCoins(acceptableCoins, Amount(amount.quantity, currency)) + val (gathered, gatheredAmount) = gatherCoins(acceptableCoins, amount) val takeChangeFrom = gathered.lastOrNull() val change = if (takeChangeFrom != null && gatheredAmount > amount) { Amount(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.data.amount.token) diff --git a/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt b/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt index c80903f95a..8a42262ef6 100644 --- a/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt @@ -33,7 +33,7 @@ class CashExitFlow(val amount: Amount, val issueRef: OpaqueBytes, prog progressTracker.currentStep = GENERATING_TX val builder: TransactionBuilder = TransactionType.General.Builder(null) val issuer = serviceHub.myInfo.legalIdentity.ref(issueRef) - val exitStates = serviceHub.vaultService.unconsumedStatesForSpending(amount, setOf(issuer.party), builder.notary, builder.lockId) + val exitStates = serviceHub.vaultService.unconsumedStatesForSpending(amount, setOf(issuer.party), builder.notary, builder.lockId, setOf(issuer.reference)) try { Cash().generateExit( builder, diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index 58b4a2dc5c..74163bd403 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -13,19 +13,13 @@ import net.corda.contracts.asset.Cash import net.corda.core.ThreadBox import net.corda.core.bufferUntilSubscribed import net.corda.core.contracts.* -import net.corda.core.crypto.AbstractParty -import net.corda.core.crypto.CompositeKey -import net.corda.core.crypto.Party -import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.* import net.corda.core.node.ServiceHub import net.corda.core.node.services.StatesNotAvailableException import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultService import net.corda.core.node.services.unconsumedStates -import net.corda.core.serialization.SingletonSerializeAsToken -import net.corda.core.serialization.deserialize -import net.corda.core.serialization.serialize -import net.corda.core.serialization.storageKryo +import net.corda.core.serialization.* import net.corda.core.tee import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction @@ -326,9 +320,11 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P val spendLock: ReentrantLock = ReentrantLock() @Suspendable - override fun unconsumedStatesForSpending(amount: Amount, onlyFromIssuerParties: Set?, notary: Party?, lockId: UUID): List> { + override fun unconsumedStatesForSpending(amount: Amount, onlyFromIssuerParties: Set?, notary: Party?, lockId: UUID, withIssuerRefs: Set?): List> { val issuerKeysStr = onlyFromIssuerParties?.fold("") { left, right -> left + "('${right.owningKey.toBase58String()}')," }?.dropLast(1) + val issuerRefsStr = withIssuerRefs?.fold("") { left, right -> left + "('${right.bytes.toHexString()}')," }?.dropLast(1) + var stateAndRefs = mutableListOf>() // TODO: Need to provide a database provider independent means of performing this function. @@ -359,7 +355,9 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P (if (notary != null) " AND vs.notary_key = '${notary.owningKey.toBase58String()}'" else "") + (if (issuerKeysStr != null) - " AND ccs.issuer_key IN $issuerKeysStr" else "") + " AND ccs.issuer_key IN ($issuerKeysStr)" else "") + + (if (issuerRefsStr != null) + " AND ccs.issuer_ref IN ($issuerRefsStr)" else "") // Retrieve spendable state refs val rs = statement.executeQuery(selectJoin) diff --git a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt index 225f39aff2..d1dd8ed608 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt @@ -1,6 +1,7 @@ package net.corda.node.services.vault import net.corda.contracts.asset.Cash +import net.corda.contracts.asset.DUMMY_CASH_ISSUER import net.corda.contracts.testing.fillWithSomeTestCash import net.corda.core.contracts.* import net.corda.core.crypto.composite @@ -8,11 +9,14 @@ import net.corda.core.node.services.StatesNotAvailableException import net.corda.core.node.services.TxWritableStorageService import net.corda.core.node.services.VaultService import net.corda.core.node.services.unconsumedStates +import net.corda.core.serialization.OpaqueBytes import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.DUMMY_NOTARY import net.corda.core.utilities.LogHelper import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.databaseTransaction +import net.corda.testing.BOC +import net.corda.testing.BOC_KEY import net.corda.testing.MEGA_CORP import net.corda.testing.MEGA_CORP_KEY import net.corda.testing.node.MockServices @@ -304,6 +308,43 @@ class NodeVaultServiceTest { } } + @Test + fun `unconsumedStatesForSpending from two issuer parties`() { + databaseTransaction(database) { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (DUMMY_CASH_ISSUER)) + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(1)), issuerKey = BOC_KEY) + + val spendableStatesUSD = services.vaultService.unconsumedStatesForSpending(200.DOLLARS, lockId = UUID.randomUUID(), + onlyFromIssuerParties = setOf(DUMMY_CASH_ISSUER.party, BOC)).toList() + spendableStatesUSD.forEach(::println) + assertThat(spendableStatesUSD).hasSize(2) + assertThat(spendableStatesUSD[0].state.data.amount.token.issuer).isEqualTo(DUMMY_CASH_ISSUER) + assertThat(spendableStatesUSD[1].state.data.amount.token.issuer).isEqualTo(BOC.ref(1)) + } + } + + @Test + fun `unconsumedStatesForSpending from specific issuer party and refs`() { + databaseTransaction(database) { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (DUMMY_CASH_ISSUER)) + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(1)), issuerKey = BOC_KEY, ref = OpaqueBytes.of(1)) + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(2)), issuerKey = BOC_KEY, ref = OpaqueBytes.of(2)) + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(3)), issuerKey = BOC_KEY, ref = OpaqueBytes.of(3)) + + val unconsumedStates = services.vaultService.unconsumedStates().toList() + assertThat(unconsumedStates).hasSize(4) + + val spendableStatesUSD = services.vaultService.unconsumedStatesForSpending(200.DOLLARS, lockId = UUID.randomUUID(), + onlyFromIssuerParties = setOf(BOC), withIssuerRefs = setOf(OpaqueBytes.of(1), OpaqueBytes.of(2))).toList() + assertThat(spendableStatesUSD).hasSize(2) + assertThat(spendableStatesUSD[0].state.data.amount.token.issuer.party).isEqualTo(BOC) + assertThat(spendableStatesUSD[0].state.data.amount.token.issuer.reference).isEqualTo(BOC.ref(1).reference) + assertThat(spendableStatesUSD[1].state.data.amount.token.issuer.reference).isEqualTo(BOC.ref(2).reference) + } + } + @Test fun `unconsumedStatesForSpending insufficient amount`() { databaseTransaction(database) {