Make a generic generate spend. Tests pass.

Fixup after rebase

Fixup after rebase

Make node have only test compile dependency against finance module.

Use original query code.

Fixup after rebase

Update docs

Edit docs

Add to changelog

Follow recommendations from PR

Follow recommendations from PR

Make a re-usable helper function to create MockServices with database for tests

Tweak a few comments

Don't include tryLockFungibleStateForSpending in soft lock docs.

Respond to PR comments

Fix whitespace error

Fix compile error

Fixup after rebase
This commit is contained in:
Matthew Nesbit 2017-08-03 17:17:17 +01:00
parent 9103b3b962
commit f4f2d35375
27 changed files with 577 additions and 464 deletions

View File

@ -5,18 +5,13 @@ import net.corda.core.concurrent.CordaFuture
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowException import net.corda.core.flows.FlowException
import net.corda.core.identity.AbstractParty import net.corda.core.node.services.vault.QueryCriteria
import net.corda.core.identity.Party
import net.corda.core.messaging.DataFeed
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.toFuture import net.corda.core.toFuture
import net.corda.core.transactions.CoreTransaction import net.corda.core.transactions.CoreTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.NonEmptySet import net.corda.core.utilities.NonEmptySet
import net.corda.core.utilities.OpaqueBytes
import rx.Observable import rx.Observable
import rx.subjects.PublishSubject import rx.subjects.PublishSubject
import java.security.PublicKey
import java.time.Instant import java.time.Instant
import java.util.* import java.util.*
@ -224,29 +219,7 @@ interface VaultService {
fun getTransactionNotes(txnId: SecureHash): Iterable<String> fun getTransactionNotes(txnId: SecureHash): Iterable<String>
/** // DOCEND VaultStatesQuery
* Generate a transaction that moves an amount of currency to the given pubkey.
*
* Note: an [Amount] of [Currency] is only fungible for a given Issuer Party within a [FungibleAsset]
*
* @param tx A builder, which may contain inputs, outputs and commands already. The relevant components needed
* to move the cash will be added on top.
* @param amount How much currency to send.
* @param to a key of the recipient.
* @param onlyFromParties if non-null, the asset states will be filtered to only include those issued by the set
* of given parties. This can be useful if the party you're trying to pay has expectations
* about which type of asset claims they are willing to accept.
* @return A [Pair] of the same transaction builder passed in as [tx], and the list of keys that need to sign
* the resulting transaction for it to be valid.
* @throws InsufficientBalanceException when a cash spending transaction fails because
* there is insufficient quantity for a given currency (and optionally set of Issuer Parties).
*/
@Throws(InsufficientBalanceException::class)
@Suspendable
fun generateSpend(tx: TransactionBuilder,
amount: Amount<Currency>,
to: AbstractParty,
onlyFromParties: Set<AbstractParty>? = null): Pair<TransactionBuilder, List<PublicKey>>
/** /**
* Soft locking is used to prevent multiple transactions trying to use the same output simultaneously. * Soft locking is used to prevent multiple transactions trying to use the same output simultaneously.
@ -257,7 +230,10 @@ interface VaultService {
/** /**
* Reserve a set of [StateRef] for a given [UUID] unique identifier. * Reserve a set of [StateRef] for a given [UUID] unique identifier.
* Typically, the unique identifier will refer to a Flow lockId associated with a [Transaction] in an in-flight flow. * Typically, the unique identifier will refer to a [FlowLogic.runId.uuid] associated with an in-flight flow.
* In this case if the flow terminates the locks will automatically be freed, even if there is an error.
* However, the user can specify their own [UUID] and manage this manually, possibly across the lifetime of multiple flows,
* or from other thread contexts e.g. [CordaService] instances.
* In the case of coin selection, soft locks are automatically taken upon gathering relevant unconsumed input refs. * In the case of coin selection, soft locks are automatically taken upon gathering relevant unconsumed input refs.
* *
* @throws [StatesNotAvailableException] when not possible to softLock all of requested [StateRef] * @throws [StatesNotAvailableException] when not possible to softLock all of requested [StateRef]
@ -273,21 +249,32 @@ interface VaultService {
* are consumed as part of cash spending. * are consumed as part of cash spending.
*/ */
fun softLockRelease(lockId: UUID, stateRefs: NonEmptySet<StateRef>? = null) fun softLockRelease(lockId: UUID, stateRefs: NonEmptySet<StateRef>? = null)
// DOCEND SoftLockAPI // DOCEND SoftLockAPI
/** /**
* TODO: this function should be private to the vault, but currently Cash Exit functionality * Helper function to combine using [VaultQueryService] calls to determine spendable states and soft locking them.
* is implemented in a separate module (finance) and requires access to it. * Currently performance will be worse than for the hand optimised version in `Cash.unconsumedCashStatesForSpending`
* However, this is fully generic and can operate with custom [FungibleAsset] states.
* @param lockId The [FlowLogic.runId.uuid] of the current flow used to soft lock the states.
* @param eligibleStatesQuery A custom query object that selects down to the appropriate subset of all states of the
* [contractType]. e.g. by selecting on account, issuer, etc. The query is internally augmented with the UNCONSUMED,
* soft lock and contract type requirements.
* @param amount The required amount of the asset, but with the issuer stripped off.
* It is assumed that compatible issuer states will be filtered out by the [eligibleStatesQuery].
* @param contractType class type of the result set.
* @return Returns a locked subset of the [eligibleStatesQuery] sufficient to satisfy the requested amount,
* or else an empty list and no change in the stored lock states when their are insufficient resources available.
*/ */
@Suspendable @Suspendable
fun <T : ContractState> unconsumedStatesForSpending(amount: Amount<Currency>, @Throws(StatesNotAvailableException::class)
onlyFromIssuerParties: Set<AbstractParty>? = null, fun <T : FungibleAsset<U>, U : Any> tryLockFungibleStatesForSpending(lockId: UUID,
notary: Party? = null, eligibleStatesQuery: QueryCriteria,
lockId: UUID, amount: Amount<U>,
withIssuerRefs: Set<OpaqueBytes>? = null): List<StateAndRef<T>> contractType: Class<out T>): List<StateAndRef<T>>
} }
class StatesNotAvailableException(override val message: String?, override val cause: Throwable? = null) : FlowException(message, cause) { class StatesNotAvailableException(override val message: String?, override val cause: Throwable? = null) : FlowException(message, cause) {
override fun toString() = "Soft locking error: $message" override fun toString() = "Soft locking error: $message"
} }

View File

@ -60,6 +60,10 @@ UNRELEASED
* ``Cordformation`` adds a ``corda`` and ``cordaRuntime`` configuration to projects which cordapp developers should * ``Cordformation`` adds a ``corda`` and ``cordaRuntime`` configuration to projects which cordapp developers should
use to exclude core Corda JARs from being built into Cordapp fat JARs. use to exclude core Corda JARs from being built into Cordapp fat JARs.
* Move the original ``Cash`` specific ``generateSpend`` and ``unconsumedStatesForSpending`` methods from ``:core``
and onto ``Cash`` contract in the ``:finance`` module. Provide a genuinely generic ``tryLockFungibleStatesForSpending``
on ``VaultService``, which in future could be optimised for performance.
.. Milestone 15: .. Milestone 15:
* Vault Query fix: filter by multiple issuer names in ``FungibleAssetQueryCriteria`` * Vault Query fix: filter by multiple issuer names in ``FungibleAssetQueryCriteria``

View File

@ -5,6 +5,7 @@ import net.corda.contracts.asset.Cash
import net.corda.core.contracts.Amount import net.corda.core.contracts.Amount
import net.corda.core.contracts.Issued import net.corda.core.contracts.Issued
import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.withoutIssuer
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.TransactionSignature import net.corda.core.crypto.TransactionSignature
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
@ -13,7 +14,6 @@ import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.* import net.corda.core.flows.*
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.node.ServiceHub import net.corda.core.node.ServiceHub
import net.corda.core.node.services.queryBy
import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.node.services.vault.QueryCriteria
import net.corda.core.node.services.vault.builder import net.corda.core.node.services.vault.builder
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
@ -31,9 +31,10 @@ private data class FxRequest(val tradeId: String,
val notary: Party? = null) val notary: Party? = null)
// DOCSTART 1 // DOCSTART 1
// This is equivalent to the VaultService.generateSpend // This is equivalent to the Cash.generateSpend
// Which is brought here to make the filtering logic more visible in the example // Which is brought here to make the filtering logic more visible in the example
private fun gatherOurInputs(serviceHub: ServiceHub, private fun gatherOurInputs(serviceHub: ServiceHub,
lockId: UUID,
amountRequired: Amount<Issued<Currency>>, amountRequired: Amount<Issued<Currency>>,
notary: Party?): Pair<List<StateAndRef<Cash.State>>, Long> { notary: Party?): Pair<List<StateAndRef<Cash.State>>, Long> {
// extract our identity for convenience // extract our identity for convenience
@ -41,34 +42,25 @@ private fun gatherOurInputs(serviceHub: ServiceHub,
val ourParties = ourKeys.map { serviceHub.identityService.partyFromKey(it) ?: throw IllegalStateException("Unable to resolve party from key") } val ourParties = ourKeys.map { serviceHub.identityService.partyFromKey(it) ?: throw IllegalStateException("Unable to resolve party from key") }
val fungibleCriteria = QueryCriteria.FungibleAssetQueryCriteria(owner = ourParties) val fungibleCriteria = QueryCriteria.FungibleAssetQueryCriteria(owner = ourParties)
val notaryName = if (notary != null) notary.name else serviceHub.networkMapCache.getAnyNotary()!!.name
val vaultCriteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(notaryName = listOf(notaryName))
val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.equal(amountRequired.token.product.currencyCode) } val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.equal(amountRequired.token.product.currencyCode) }
val cashCriteria = QueryCriteria.VaultCustomQueryCriteria(logicalExpression) val cashCriteria = QueryCriteria.VaultCustomQueryCriteria(logicalExpression)
// Collect cash type inputs val fullCriteria = fungibleCriteria.and(vaultCriteria).and(cashCriteria)
val suitableCashStates = serviceHub.vaultQueryService.queryBy<Cash.State>(fungibleCriteria.and(cashCriteria)).states
require(!suitableCashStates.isEmpty()) { "Insufficient funds" }
var remaining = amountRequired.quantity val eligibleStates = serviceHub.vaultService.tryLockFungibleStatesForSpending<Cash.State, Currency>(lockId, fullCriteria, amountRequired.withoutIssuer(), Cash.State::class.java)
// We will need all of the inputs to be on the same notary.
// For simplicity we just filter on the first notary encountered
// A production quality flow would need to migrate notary if the
// the amounts were not sufficient in any one notary
val sourceNotary: Party = notary ?: suitableCashStates.first().state.notary
val inputsList = mutableListOf<StateAndRef<Cash.State>>() check(eligibleStates.isNotEmpty()) { "Insufficient funds" }
// Iterate over filtered cash states to gather enough to pay val amount = eligibleStates.fold(0L) { tot, x -> tot + x.state.data.amount.quantity }
for (cash in suitableCashStates.filter { it.state.notary == sourceNotary }) { val change = amount - amountRequired.quantity
inputsList += cash
if (remaining <= cash.state.data.amount.quantity) { return Pair(eligibleStates, change)
return Pair(inputsList, cash.state.data.amount.quantity - remaining)
}
remaining -= cash.state.data.amount.quantity
}
throw IllegalStateException("Insufficient funds")
} }
// DOCEND 1 // DOCEND 1
private fun prepareOurInputsAndOutputs(serviceHub: ServiceHub, request: FxRequest): Pair<List<StateAndRef<Cash.State>>, List<Cash.State>> { private fun prepareOurInputsAndOutputs(serviceHub: ServiceHub, lockId: UUID, request: FxRequest): Pair<List<StateAndRef<Cash.State>>, List<Cash.State>> {
// Create amount with correct issuer details // Create amount with correct issuer details
val sellAmount = request.amount val sellAmount = request.amount
@ -78,7 +70,7 @@ private fun prepareOurInputsAndOutputs(serviceHub: ServiceHub, request: FxReques
// we will use query manually in the helper function below. // we will use query manually in the helper function below.
// Putting this into a non-suspendable function also prevents issues when // Putting this into a non-suspendable function also prevents issues when
// the flow is suspended. // the flow is suspended.
val (inputs, residual) = gatherOurInputs(serviceHub, sellAmount, request.notary) val (inputs, residual) = gatherOurInputs(serviceHub, lockId, sellAmount, request.notary)
// Build and an output state for the counterparty // Build and an output state for the counterparty
val transferedFundsOutput = Cash.State(sellAmount, request.counterparty) val transferedFundsOutput = Cash.State(sellAmount, request.counterparty)
@ -119,7 +111,7 @@ class ForeignExchangeFlow(val tradeId: String,
} else throw IllegalArgumentException("Our identity must be one of the parties in the trade.") } else throw IllegalArgumentException("Our identity must be one of the parties in the trade.")
// Call the helper method to identify suitable inputs and make the outputs // Call the helper method to identify suitable inputs and make the outputs
val (outInputStates, ourOutputStates) = prepareOurInputsAndOutputs(serviceHub, localRequest) val (outInputStates, ourOutputStates) = prepareOurInputsAndOutputs(serviceHub, runId.uuid, localRequest)
// identify the notary for our states // identify the notary for our states
val notary = outInputStates.first().state.notary val notary = outInputStates.first().state.notary
@ -231,7 +223,7 @@ class ForeignExchangeRemoteFlow(val source: Party) : FlowLogic<Unit>() {
// we will use query manually in the helper function below. // we will use query manually in the helper function below.
// Putting this into a non-suspendable function also prevent issues when // Putting this into a non-suspendable function also prevent issues when
// the flow is suspended. // the flow is suspended.
val (ourInputState, ourOutputState) = prepareOurInputsAndOutputs(serviceHub, request) val (ourInputState, ourOutputState) = prepareOurInputsAndOutputs(serviceHub, runId.uuid, request)
// Send back our proposed states and await the full transaction to verify // Send back our proposed states and await the full transaction to verify
val ourKey = serviceHub.keyManagementService.filterMyKeys(ourInputState.flatMap { it.state.data.participants }.map { it.owningKey }).single() val ourKey = serviceHub.keyManagementService.filterMyKeys(ourInputState.flatMap { it.state.data.participants }.map { it.owningKey }).single()

View File

@ -279,7 +279,7 @@ This code is longer but no more complicated. Here are some things to pay attenti
1. We do some sanity checking on the proposed trade transaction received from the seller to ensure we're being offered 1. We do some sanity checking on the proposed trade transaction received from the seller to ensure we're being offered
what we expected to be offered. what we expected to be offered.
2. We create a cash spend using ``VaultService.generateSpend``. You can read the vault documentation to learn more about this. 2. We create a cash spend using ``Cash.generateSpend``. You can read the vault documentation to learn more about this.
3. We access the *service hub* as needed to access things that are transient and may change or be recreated 3. We access the *service hub* as needed to access things that are transient and may change or be recreated
whilst a flow is suspended, such as the wallet or the network map. whilst a flow is suspended, such as the wallet or the network map.
4. We call ``CollectSignaturesFlow`` as a subflow to send the unfinished, still-invalid transaction to the seller so 4. We call ``CollectSignaturesFlow`` as a subflow to send the unfinished, still-invalid transaction to the seller so

View File

@ -116,11 +116,12 @@ standard ``CashState`` in the ``:financial`` Gradle module. The Cash
contract uses ``FungibleAsset`` states to model holdings of contract uses ``FungibleAsset`` states to model holdings of
interchangeable assets and allow the split/merge and summing of interchangeable assets and allow the split/merge and summing of
states to meet a contractual obligation. We would normally use the states to meet a contractual obligation. We would normally use the
``generateSpend`` method on the ``VaultService`` to gather the required ``Cash.generateSpend`` method to gather the required
amount of cash into a ``TransactionBuilder``, set the outputs and move amount of cash into a ``TransactionBuilder``, set the outputs and move
command. However, to elucidate more clearly example flow code is shown command. However, to elucidate more clearly example flow code is shown
here that will manually carry out the inputs queries by specifying relevant here that will manually carry out the inputs queries by specifying relevant
query criteria filters to the ``queryBy`` method of the ``VaultQueryService``. query criteria filters to the ``tryLockFungibleStatesForSpending`` method
of the ``VaultService``.
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt
:language: kotlin :language: kotlin

View File

@ -680,9 +680,9 @@ Finally, we can do redemption.
.. sourcecode:: kotlin .. sourcecode:: kotlin
@Throws(InsufficientBalanceException::class) @Throws(InsufficientBalanceException::class)
fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef<State>, vault: VaultService) { fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef<State>, services: ServiceHub) {
// Add the cash movement using the states in our vault. // Add the cash movement using the states in our vault.
vault.generateSpend(tx, paper.state.data.faceValue.withoutIssuer(), paper.state.data.owner) Cash.generateSpend(services, tx, paper.state.data.faceValue.withoutIssuer(), paper.state.data.owner)
tx.addInputState(paper) tx.addInputState(paper)
tx.addCommand(Command(Commands.Redeem(), paper.state.data.owner.owningKey)) tx.addCommand(Command(Commands.Redeem(), paper.state.data.owner.owningKey))
} }
@ -698,7 +698,7 @@ from the issuer of the commercial paper to the current owner. If we don't have e
an exception is thrown. Then we add the paper itself as an input, but, not an output (as we wish to remove it an exception is thrown. Then we add the paper itself as an input, but, not an output (as we wish to remove it
from the ledger). Finally, we add a Redeem command that should be signed by the owner of the commercial paper. from the ledger). Finally, we add a Redeem command that should be signed by the owner of the commercial paper.
.. warning:: The amount we pass to the ``generateSpend`` function has to be treated first with ``withoutIssuer``. .. warning:: The amount we pass to the ``Cash.generateSpend`` function has to be treated first with ``withoutIssuer``.
This reflects the fact that the way we handle issuer constraints is still evolving; the commercial paper This reflects the fact that the way we handle issuer constraints is still evolving; the commercial paper
contract requires payment in the form of a currency issued by a specific party (e.g. the central bank, contract requires payment in the form of a currency issued by a specific party (e.g. the central bank,
or the issuers own bank perhaps). But the vault wants to assemble spend transactions using cash states from or the issuers own bank perhaps). But the vault wants to assemble spend transactions using cash states from

View File

@ -5,6 +5,7 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import kotlin.Pair; import kotlin.Pair;
import kotlin.Unit; import kotlin.Unit;
import net.corda.contracts.asset.Cash;
import net.corda.contracts.asset.CashKt; import net.corda.contracts.asset.CashKt;
import net.corda.core.contracts.*; import net.corda.core.contracts.*;
import net.corda.core.crypto.SecureHash; import net.corda.core.crypto.SecureHash;
@ -12,15 +13,14 @@ import net.corda.core.crypto.testing.NullPublicKey;
import net.corda.core.identity.AbstractParty; import net.corda.core.identity.AbstractParty;
import net.corda.core.identity.AnonymousParty; import net.corda.core.identity.AnonymousParty;
import net.corda.core.identity.Party; import net.corda.core.identity.Party;
import net.corda.core.node.services.VaultService; import net.corda.core.node.ServiceHub;
import net.corda.core.transactions.LedgerTransaction; import net.corda.core.transactions.LedgerTransaction;
import net.corda.core.transactions.TransactionBuilder; import net.corda.core.transactions.TransactionBuilder;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.time.Instant; import java.time.Instant;
import java.util.Currency; import java.util.*;
import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static net.corda.core.contracts.ContractsDSL.requireSingleCommand; import static net.corda.core.contracts.ContractsDSL.requireSingleCommand;
@ -255,8 +255,8 @@ public class JavaCommercialPaper implements Contract {
} }
@Suspendable @Suspendable
public void generateRedeem(TransactionBuilder tx, StateAndRef<State> paper, VaultService vault) throws InsufficientBalanceException { public void generateRedeem(TransactionBuilder tx, StateAndRef<State> paper, ServiceHub services) throws InsufficientBalanceException {
vault.generateSpend(tx, Structures.withoutIssuer(paper.getState().getData().getFaceValue()), paper.getState().getData().getOwner(), null); Cash.generateSpend(services, tx, Structures.withoutIssuer(paper.getState().getData().getFaceValue()), paper.getState().getData().getOwner(), Collections.EMPTY_SET);
tx.addInputState(paper); tx.addInputState(paper);
tx.addCommand(new Command<>(new Commands.Redeem(), paper.getState().getData().getOwner().getOwningKey())); tx.addCommand(new Command<>(new Commands.Redeem(), paper.getState().getData().getOwner().getOwningKey()));
} }

View File

@ -1,6 +1,7 @@
package net.corda.contracts package net.corda.contracts
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import net.corda.contracts.asset.Cash
import net.corda.contracts.asset.sumCashBy import net.corda.contracts.asset.sumCashBy
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
@ -9,7 +10,7 @@ import net.corda.core.crypto.toBase58String
import net.corda.core.identity.AbstractParty import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.internal.Emoji import net.corda.core.internal.Emoji
import net.corda.core.node.services.VaultService import net.corda.core.node.ServiceHub
import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.MappedSchema
import net.corda.core.schemas.PersistentState import net.corda.core.schemas.PersistentState
import net.corda.core.schemas.QueryableState import net.corda.core.schemas.QueryableState
@ -185,9 +186,9 @@ class CommercialPaper : Contract {
*/ */
@Throws(InsufficientBalanceException::class) @Throws(InsufficientBalanceException::class)
@Suspendable @Suspendable
fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef<State>, vault: VaultService) { fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef<State>, services: ServiceHub) {
// Add the cash movement using the states in our vault. // Add the cash movement using the states in our vault.
vault.generateSpend(tx, paper.state.data.faceValue.withoutIssuer(), paper.state.data.owner) Cash.generateSpend(services, tx, paper.state.data.faceValue.withoutIssuer(), paper.state.data.owner)
tx.addInputState(paper) tx.addInputState(paper)
tx.addCommand(Commands.Redeem(), paper.state.data.owner.owningKey) tx.addCommand(Commands.Redeem(), paper.state.data.owner.owningKey)
} }

View File

@ -1,5 +1,7 @@
package net.corda.contracts.asset package net.corda.contracts.asset
import co.paralleluniverse.fibers.Suspendable
import co.paralleluniverse.strands.Strand
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.entropyToKeyPair import net.corda.core.crypto.entropyToKeyPair
@ -9,16 +11,27 @@ import net.corda.core.crypto.toBase58String
import net.corda.core.identity.AbstractParty import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.internal.Emoji import net.corda.core.internal.Emoji
import net.corda.core.node.ServiceHub
import net.corda.core.node.services.StatesNotAvailableException
import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.MappedSchema
import net.corda.core.schemas.PersistentState import net.corda.core.schemas.PersistentState
import net.corda.core.schemas.QueryableState import net.corda.core.schemas.QueryableState
import net.corda.core.serialization.SerializationDefaults
import net.corda.core.serialization.deserialize
import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.toHexString
import net.corda.core.utilities.toNonEmptySet
import net.corda.core.utilities.trace
import net.corda.schemas.CashSchemaV1 import net.corda.schemas.CashSchemaV1
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import java.math.BigInteger import java.math.BigInteger
import java.security.PublicKey import java.security.PublicKey
import java.sql.SQLException
import java.util.* import java.util.*
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
@ -212,6 +225,163 @@ class Cash : OnLedgerAsset<Currency, Cash.Commands, Cash.State>() {
"there is only a single issue command" using (cashCommands.count() == 1) "there is only a single issue command" using (cashCommands.count() == 1)
} }
} }
companion object {
// coin selection retry loop counter, sleep (msecs) and lock for selecting states
private val MAX_RETRIES = 5
private val RETRY_SLEEP = 100
private val spendLock: ReentrantLock = ReentrantLock()
/**
* Generate a transaction that moves an amount of currency to the given pubkey.
*
* Note: an [Amount] of [Currency] is only fungible for a given Issuer Party within a [FungibleAsset]
*
* @param services The [ServiceHub] to provide access to the database session.
* @param tx A builder, which may contain inputs, outputs and commands already. The relevant components needed
* to move the cash will be added on top.
* @param amount How much currency to send.
* @param to a key of the recipient.
* @param onlyFromParties if non-null, the asset states will be filtered to only include those issued by the set
* of given parties. This can be useful if the party you're trying to pay has expectations
* about which type of asset claims they are willing to accept.
* @return A [Pair] of the same transaction builder passed in as [tx], and the list of keys that need to sign
* the resulting transaction for it to be valid.
* @throws InsufficientBalanceException when a cash spending transaction fails because
* there is insufficient quantity for a given currency (and optionally set of Issuer Parties).
*/
@JvmStatic
@Throws(InsufficientBalanceException::class)
@Suspendable
fun generateSpend(services: ServiceHub,
tx: TransactionBuilder,
amount: Amount<Currency>,
to: AbstractParty,
onlyFromParties: Set<AbstractParty> = emptySet()): Pair<TransactionBuilder, List<PublicKey>> {
fun deriveState(txState: TransactionState<Cash.State>, amt: Amount<Issued<Currency>>, owner: AbstractParty)
= txState.copy(data = txState.data.copy(amount = amt, owner = owner))
// Retrieve unspent and unlocked cash states that meet our spending criteria.
val acceptableCoins = Cash.unconsumedCashStatesForSpending(services, amount, onlyFromParties, tx.notary, tx.lockId)
return OnLedgerAsset.generateSpend(tx, amount, to, acceptableCoins,
{ state, quantity, owner -> deriveState(state, quantity, owner) },
{ Cash().generateMoveCommand() })
}
/**
* An optimised query to gather Cash states that are available and retry if they are temporarily unavailable.
* @param services The service hub to allow access to the database session
* @param amount The amount of currency desired (ignoring issues, but specifying the currency)
* @param onlyFromIssuerParties If empty the operation ignores the specifics of the issuer,
* otherwise the set of eligible states wil be filtered to only include those from these issuers.
* @param notary If null the notary source is ignored, if specified then only states marked
* with this notary are included.
* @param lockId The [FlowLogic.runId.uuid] of the flow, which is used to soft reserve the states.
* Also, previous outputs of the flow will be eligible as they are implicitly locked with this id until the flow completes.
* @param withIssuerRefs If not empty the specific set of issuer references to match against.
* @return The matching states that were found. If sufficient funds were found these will be locked,
* otherwise what is available is returned unlocked for informational purposes.
*/
@JvmStatic
@Suspendable
fun unconsumedCashStatesForSpending(services: ServiceHub,
amount: Amount<Currency>,
onlyFromIssuerParties: Set<AbstractParty> = emptySet(),
notary: Party? = null,
lockId: UUID,
withIssuerRefs: Set<OpaqueBytes> = emptySet()): List<StateAndRef<Cash.State>> {
val issuerKeysStr = onlyFromIssuerParties.fold("") { left, right -> left + "('${right.owningKey.toBase58String()}')," }.dropLast(1)
val issuerRefsStr = withIssuerRefs.fold("") { left, right -> left + "('${right.bytes.toHexString()}')," }.dropLast(1)
val stateAndRefs = mutableListOf<StateAndRef<Cash.State>>()
// TODO: Need to provide a database provider independent means of performing this function.
// We are using an H2 specific means of selecting a minimum set of rows that match a request amount of coins:
// 1) There is no standard SQL mechanism of calculating a cumulative total on a field and restricting row selection on the
// running total of such an accumulator
// 2) H2 uses session variables to perform this accumulator function:
// http://www.h2database.com/html/functions.html#set
// 3) H2 does not support JOIN's in FOR UPDATE (hence we are forced to execute 2 queries)
for (retryCount in 1..MAX_RETRIES) {
spendLock.withLock {
val statement = services.jdbcSession().createStatement()
try {
statement.execute("CALL SET(@t, 0);")
// we select spendable states irrespective of lock but prioritised by unlocked ones (Eg. null)
// the softLockReserve update will detect whether we try to lock states locked by others
val selectJoin = """
SELECT vs.transaction_id, vs.output_index, vs.contract_state, ccs.pennies, SET(@t, ifnull(@t,0)+ccs.pennies) total_pennies, vs.lock_id
FROM vault_states AS vs, contract_cash_states AS ccs
WHERE vs.transaction_id = ccs.transaction_id AND vs.output_index = ccs.output_index
AND vs.state_status = 0
AND ccs.ccy_code = '${amount.token}' and @t < ${amount.quantity}
AND (vs.lock_id = '$lockId' OR vs.lock_id is null)
""" +
(if (notary != null)
" AND vs.notary_key = '${notary.owningKey.toBase58String()}'" else "") +
(if (onlyFromIssuerParties.isNotEmpty())
" AND ccs.issuer_key IN ($issuerKeysStr)" else "") +
(if (withIssuerRefs.isNotEmpty())
" AND ccs.issuer_ref IN ($issuerRefsStr)" else "")
// Retrieve spendable state refs
val rs = statement.executeQuery(selectJoin)
stateAndRefs.clear()
log.debug(selectJoin)
var totalPennies = 0L
while (rs.next()) {
val txHash = SecureHash.parse(rs.getString(1))
val index = rs.getInt(2)
val stateRef = StateRef(txHash, index)
val state = rs.getBytes(3).deserialize<TransactionState<Cash.State>>(context = SerializationDefaults.STORAGE_CONTEXT)
val pennies = rs.getLong(4)
totalPennies = rs.getLong(5)
val rowLockId = rs.getString(6)
stateAndRefs.add(StateAndRef(state, stateRef))
log.trace { "ROW: $rowLockId ($lockId): $stateRef : $pennies ($totalPennies)" }
}
if (stateAndRefs.isNotEmpty() && totalPennies >= amount.quantity) {
// we should have a minimum number of states to satisfy our selection `amount` criteria
log.trace("Coin selection for $amount retrieved ${stateAndRefs.count()} states totalling $totalPennies pennies: $stateAndRefs")
// With the current single threaded state machine available states are guaranteed to lock.
// TODO However, we will have to revisit these methods in the future multi-threaded.
services.vaultService.softLockReserve(lockId, (stateAndRefs.map { it.ref }).toNonEmptySet())
return stateAndRefs
}
log.trace("Coin selection requested $amount but retrieved $totalPennies pennies with state refs: ${stateAndRefs.map { it.ref }}")
// retry as more states may become available
} catch (e: SQLException) {
log.error("""Failed retrieving unconsumed states for: amount [$amount], onlyFromIssuerParties [$onlyFromIssuerParties], notary [$notary], lockId [$lockId]
$e.
""")
} catch (e: StatesNotAvailableException) { // Should never happen with single threaded state machine
stateAndRefs.clear()
log.warn(e.message)
// retry only if there are locked states that may become available again (or consumed with change)
} finally {
statement.close()
}
}
log.warn("Coin selection failed on attempt $retryCount")
// TODO: revisit the back off strategy for contended spending.
if (retryCount != MAX_RETRIES) {
Strand.sleep(RETRY_SLEEP * retryCount.toLong())
}
}
log.warn("Insufficient spendable states identified for $amount")
return stateAndRefs
}
}
} }
// Small DSL extensions. // Small DSL extensions.

View File

@ -226,8 +226,6 @@ abstract class OnLedgerAsset<T : Any, C : CommandData, S : FungibleAsset<T>> : C
* *
* @param tx transaction builder to add states and commands to. * @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 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 assetStates the asset states to take funds from. No checks are done about ownership of these states, it is * @param assetStates the asset 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. * the responsibility of the caller to check that they do not exit funds held by others.
* @return the public keys which must sign the transaction for it to be valid. * @return the public keys which must sign the transaction for it to be valid.

View File

@ -11,8 +11,8 @@ import net.corda.core.node.services.queryBy
import net.corda.core.node.services.vault.DEFAULT_PAGE_NUM import net.corda.core.node.services.vault.DEFAULT_PAGE_NUM
import net.corda.core.node.services.vault.PageSpecification import net.corda.core.node.services.vault.PageSpecification
import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.ProgressTracker
import java.util.* import java.util.*
@ -41,7 +41,7 @@ class CashExitFlow(val amount: Amount<Currency>, val issueRef: OpaqueBytes, prog
progressTracker.currentStep = GENERATING_TX progressTracker.currentStep = GENERATING_TX
val builder: TransactionBuilder = TransactionBuilder(notary = null as Party?) val builder: TransactionBuilder = TransactionBuilder(notary = null as Party?)
val issuer = serviceHub.myInfo.legalIdentity.ref(issueRef) val issuer = serviceHub.myInfo.legalIdentity.ref(issueRef)
val exitStates = serviceHub.vaultService.unconsumedStatesForSpending<Cash.State>(amount, setOf(issuer.party), builder.notary, builder.lockId, setOf(issuer.reference)) val exitStates = Cash.unconsumedCashStatesForSpending(serviceHub, amount, setOf(issuer.party), builder.notary, builder.lockId, setOf(issuer.reference))
val signers = try { val signers = try {
Cash().generateExit( Cash().generateExit(
builder, builder,

View File

@ -1,6 +1,7 @@
package net.corda.flows package net.corda.flows
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import net.corda.contracts.asset.Cash
import net.corda.core.contracts.Amount import net.corda.core.contracts.Amount
import net.corda.core.contracts.InsufficientBalanceException import net.corda.core.contracts.InsufficientBalanceException
import net.corda.core.flows.StartableByRPC import net.corda.core.flows.StartableByRPC
@ -26,7 +27,7 @@ open class CashPaymentFlow(
val recipient: Party, val recipient: Party,
val anonymous: Boolean, val anonymous: Boolean,
progressTracker: ProgressTracker, progressTracker: ProgressTracker,
val issuerConstraint: Set<Party>? = null) : AbstractCashFlow<AbstractCashFlow.Result>(progressTracker) { val issuerConstraint: Set<Party> = emptySet()) : AbstractCashFlow<AbstractCashFlow.Result>(progressTracker) {
/** A straightforward constructor that constructs spends using cash states of any issuer. */ /** A straightforward constructor that constructs spends using cash states of any issuer. */
constructor(amount: Amount<Currency>, recipient: Party) : this(amount, recipient, true, tracker()) constructor(amount: Amount<Currency>, recipient: Party) : this(amount, recipient, true, tracker())
/** A straightforward constructor that constructs spends using cash states of any issuer. */ /** A straightforward constructor that constructs spends using cash states of any issuer. */
@ -45,7 +46,7 @@ open class CashPaymentFlow(
val builder: TransactionBuilder = TransactionBuilder(null as Party?) val builder: TransactionBuilder = TransactionBuilder(null as Party?)
// TODO: Have some way of restricting this to states the caller controls // TODO: Have some way of restricting this to states the caller controls
val (spendTX, keysForSigning) = try { val (spendTX, keysForSigning) = try {
serviceHub.vaultService.generateSpend( Cash.generateSpend(serviceHub,
builder, builder,
amount, amount,
anonymousRecipient, anonymousRecipient,

View File

@ -1,8 +1,12 @@
package net.corda.flows package net.corda.flows
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import net.corda.contracts.asset.Cash
import net.corda.contracts.asset.sumCashBy import net.corda.contracts.asset.sumCashBy
import net.corda.core.contracts.* import net.corda.core.contracts.Amount
import net.corda.core.contracts.OwnableState
import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.withoutIssuer
import net.corda.core.flows.* import net.corda.core.flows.*
import net.corda.core.identity.AbstractParty import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty import net.corda.core.identity.AnonymousParty
@ -174,7 +178,7 @@ object TwoPartyTradeFlow {
val ptx = TransactionBuilder(notary) val ptx = TransactionBuilder(notary)
// Add input and output states for the movement of cash, by using the Cash contract to generate the states // Add input and output states for the movement of cash, by using the Cash contract to generate the states
val (tx, cashSigningPubKeys) = serviceHub.vaultService.generateSpend(ptx, tradeRequest.price, tradeRequest.sellerOwner) val (tx, cashSigningPubKeys) = Cash.generateSpend(serviceHub, ptx, tradeRequest.price, tradeRequest.sellerOwner)
// Add inputs/outputs/a command for the movement of the asset. // Add inputs/outputs/a command for the movement of the asset.
tx.addInputState(assetForSale) tx.addInputState(assetForSale)

View File

@ -1,21 +1,19 @@
package net.corda.contracts package net.corda.contracts
import net.corda.contracts.asset.* import net.corda.contracts.asset.*
import net.corda.testing.contracts.fillWithSomeTestCash
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.utilities.days
import net.corda.core.identity.AnonymousParty import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.node.services.Vault import net.corda.core.node.services.Vault
import net.corda.core.node.services.VaultService import net.corda.core.node.services.VaultService
import net.corda.core.utilities.seconds
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.node.utilities.configureDatabase import net.corda.core.utilities.days
import net.corda.core.utilities.seconds
import net.corda.testing.* import net.corda.testing.*
import net.corda.testing.contracts.fillWithSomeTestCash
import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices
import net.corda.testing.node.makeTestDataSourceProperties import net.corda.testing.node.makeTestDatabaseAndMockServices
import net.corda.testing.node.makeTestDatabaseProperties
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.runners.Parameterized import org.junit.runners.Parameterized
@ -212,40 +210,22 @@ class CommercialPaperTestsGeneric {
@Test @Test
fun `issue move and then redeem`() { fun `issue move and then redeem`() {
initialiseTestSerialization() initialiseTestSerialization()
val dataSourcePropsAlice = makeTestDataSourceProperties() val aliceDatabaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(ALICE_KEY))
val databaseAlice = configureDatabase(dataSourcePropsAlice, makeTestDatabaseProperties()) val databaseAlice = aliceDatabaseAndServices.first
aliceServices = aliceDatabaseAndServices.second
aliceVaultService = aliceServices.vaultService
databaseAlice.transaction { databaseAlice.transaction {
aliceServices = object : MockServices(ALICE_KEY) {
override val vaultService: VaultService = makeVaultService(dataSourcePropsAlice)
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
for (stx in txs) {
validatedTransactions.addTransaction(stx)
}
// Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions.
vaultService.notifyAll(txs.map { it.tx })
}
}
alicesVault = aliceServices.fillWithSomeTestCash(9000.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1) alicesVault = aliceServices.fillWithSomeTestCash(9000.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1)
aliceVaultService = aliceServices.vaultService aliceVaultService = aliceServices.vaultService
} }
val dataSourcePropsBigCorp = makeTestDataSourceProperties() val bigCorpDatabaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(BIG_CORP_KEY))
val databaseBigCorp = configureDatabase(dataSourcePropsBigCorp, makeTestDatabaseProperties()) val databaseBigCorp = bigCorpDatabaseAndServices.first
bigCorpServices = bigCorpDatabaseAndServices.second
bigCorpVaultService = bigCorpServices.vaultService
databaseBigCorp.transaction { databaseBigCorp.transaction {
bigCorpServices = object : MockServices(BIG_CORP_KEY) {
override val vaultService: VaultService = makeVaultService(dataSourcePropsBigCorp)
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
for (stx in txs) {
validatedTransactions.addTransaction(stx)
}
// Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions.
vaultService.notifyAll(txs.map { it.tx })
}
}
bigCorpVault = bigCorpServices.fillWithSomeTestCash(13000.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1) bigCorpVault = bigCorpServices.fillWithSomeTestCash(13000.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1)
bigCorpVaultService = bigCorpServices.vaultService bigCorpVaultService = bigCorpServices.vaultService
} }
@ -266,7 +246,7 @@ class CommercialPaperTestsGeneric {
// Alice pays $9000 to BigCorp to own some of their debt. // Alice pays $9000 to BigCorp to own some of their debt.
moveTX = run { moveTX = run {
val builder = TransactionBuilder(DUMMY_NOTARY) val builder = TransactionBuilder(DUMMY_NOTARY)
aliceVaultService.generateSpend(builder, 9000.DOLLARS, AnonymousParty(bigCorpServices.key.public)) Cash.generateSpend(aliceServices, builder, 9000.DOLLARS, AnonymousParty(bigCorpServices.key.public))
CommercialPaper().generateMove(builder, issueTx.tx.outRef(0), AnonymousParty(aliceServices.key.public)) CommercialPaper().generateMove(builder, issueTx.tx.outRef(0), AnonymousParty(aliceServices.key.public))
val ptx = aliceServices.signInitialTransaction(builder) val ptx = aliceServices.signInitialTransaction(builder)
val ptx2 = bigCorpServices.addSignature(ptx) val ptx2 = bigCorpServices.addSignature(ptx)
@ -288,7 +268,7 @@ class CommercialPaperTestsGeneric {
fun makeRedeemTX(time: Instant): Pair<SignedTransaction, UUID> { fun makeRedeemTX(time: Instant): Pair<SignedTransaction, UUID> {
val builder = TransactionBuilder(DUMMY_NOTARY) val builder = TransactionBuilder(DUMMY_NOTARY)
builder.setTimeWindow(time, 30.seconds) builder.setTimeWindow(time, 30.seconds)
CommercialPaper().generateRedeem(builder, moveTX.tx.outRef(1), bigCorpVaultService) CommercialPaper().generateRedeem(builder, moveTX.tx.outRef(1), bigCorpServices)
val ptx = aliceServices.signInitialTransaction(builder) val ptx = aliceServices.signInitialTransaction(builder)
val ptx2 = bigCorpServices.addSignature(ptx) val ptx2 = bigCorpServices.addSignature(ptx)
val stx = notaryServices.addSignature(ptx2) val stx = notaryServices.addSignature(ptx2)

View File

@ -6,26 +6,19 @@ import net.corda.core.crypto.generateKeyPair
import net.corda.core.identity.AbstractParty import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.node.services.VaultQueryService
import net.corda.core.node.services.VaultService import net.corda.core.node.services.VaultService
import net.corda.core.node.services.queryBy import net.corda.core.node.services.queryBy
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.core.transactions.WireTransaction import net.corda.core.transactions.WireTransaction
import net.corda.node.services.database.HibernateConfiguration import net.corda.core.utilities.OpaqueBytes
import net.corda.node.services.schema.NodeSchemaService
import net.corda.node.services.vault.HibernateVaultQueryImpl
import net.corda.node.services.vault.NodeVaultService import net.corda.node.services.vault.NodeVaultService
import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.CordaPersistence
import net.corda.node.utilities.configureDatabase
import net.corda.testing.* import net.corda.testing.*
import net.corda.testing.contracts.DummyState import net.corda.testing.contracts.DummyState
import net.corda.testing.contracts.fillWithSomeTestCash import net.corda.testing.contracts.fillWithSomeTestCash
import net.corda.testing.node.MockKeyManagementService
import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices
import net.corda.testing.node.makeTestDataSourceProperties import net.corda.testing.node.makeTestDatabaseAndMockServices
import net.corda.testing.node.makeTestDatabaseProperties import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import java.security.KeyPair import java.security.KeyPair
@ -55,25 +48,11 @@ class CashTests : TestDependencyInjectionBase() {
@Before @Before
fun setUp() { fun setUp() {
LogHelper.setLevel(NodeVaultService::class) LogHelper.setLevel(NodeVaultService::class)
val dataSourceProps = makeTestDataSourceProperties() val databaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(MINI_CORP_KEY, MEGA_CORP_KEY, OUR_KEY))
database = configureDatabase(dataSourceProps, makeTestDatabaseProperties()) database = databaseAndServices.first
miniCorpServices = databaseAndServices.second
database.transaction { database.transaction {
val hibernateConfig = HibernateConfiguration(NodeSchemaService(), makeTestDatabaseProperties())
miniCorpServices = object : MockServices(MINI_CORP_KEY) {
override val keyManagementService: MockKeyManagementService = MockKeyManagementService(identityService, MINI_CORP_KEY, MEGA_CORP_KEY, OUR_KEY)
override val vaultService: VaultService = makeVaultService(dataSourceProps)
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
for (stx in txs) {
validatedTransactions.addTransaction(stx)
}
// Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions.
vaultService.notifyAll(txs.map { it.tx })
}
override val vaultQueryService: VaultQueryService = HibernateVaultQueryImpl(hibernateConfig, vaultService.updatesPublisher)
}
miniCorpServices.fillWithSomeTestCash(howMuch = 100.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1, miniCorpServices.fillWithSomeTestCash(howMuch = 100.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1,
issuedBy = MEGA_CORP.ref(1), issuerKey = MEGA_CORP_KEY, ownedBy = OUR_IDENTITY_1) issuedBy = MEGA_CORP.ref(1), issuerKey = MEGA_CORP_KEY, ownedBy = OUR_IDENTITY_1)
miniCorpServices.fillWithSomeTestCash(howMuch = 400.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1, miniCorpServices.fillWithSomeTestCash(howMuch = 400.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1,
@ -88,6 +67,11 @@ class CashTests : TestDependencyInjectionBase() {
resetTestSerialization() resetTestSerialization()
} }
@After
fun tearDown() {
database.close()
}
@Test @Test
fun trivial() { fun trivial() {
transaction { transaction {
@ -485,7 +469,7 @@ class CashTests : TestDependencyInjectionBase() {
fun makeSpend(amount: Amount<Currency>, dest: AbstractParty): WireTransaction { fun makeSpend(amount: Amount<Currency>, dest: AbstractParty): WireTransaction {
val tx = TransactionBuilder(DUMMY_NOTARY) val tx = TransactionBuilder(DUMMY_NOTARY)
database.transaction { database.transaction {
vault.generateSpend(tx, amount, dest) Cash.generateSpend(miniCorpServices, tx, amount, dest)
} }
return tx.toWireTransaction() return tx.toWireTransaction()
} }
@ -586,7 +570,7 @@ class CashTests : TestDependencyInjectionBase() {
database.transaction { database.transaction {
val tx = TransactionBuilder(DUMMY_NOTARY) val tx = TransactionBuilder(DUMMY_NOTARY)
vault.generateSpend(tx, 80.DOLLARS, ALICE, setOf(MINI_CORP)) Cash.generateSpend(miniCorpServices, tx, 80.DOLLARS, ALICE, setOf(MINI_CORP))
assertEquals(vaultStatesUnconsumed.elementAt(2).ref, tx.inputStates()[0]) assertEquals(vaultStatesUnconsumed.elementAt(2).ref, tx.inputStates()[0])
} }

View File

@ -87,7 +87,6 @@ processSmokeTestResources {
// build/reports/project/dependencies/index.html for green highlighted parts of the tree. // build/reports/project/dependencies/index.html for green highlighted parts of the tree.
dependencies { dependencies {
compile project(':finance')
compile project(':node-schemas') compile project(':node-schemas')
compile project(':node-api') compile project(':node-api')
compile project(':client:rpc') compile project(':client:rpc')
@ -150,6 +149,7 @@ dependencies {
testCompile "com.pholser:junit-quickcheck-core:$quickcheck_version" testCompile "com.pholser:junit-quickcheck-core:$quickcheck_version"
testCompile project(':test-utils') testCompile project(':test-utils')
testCompile project(':client:jfx') testCompile project(':client:jfx')
testCompile project(':finance')
// sample test schemas // sample test schemas
testCompile project(path: ':finance', configuration: 'testArtifacts') testCompile project(path: ':finance', configuration: 'testArtifacts')

View File

@ -8,13 +8,13 @@ import net.corda.core.node.services.Vault
import net.corda.core.node.services.VaultQueryException import net.corda.core.node.services.VaultQueryException
import net.corda.core.node.services.vault.* import net.corda.core.node.services.vault.*
import net.corda.core.node.services.vault.QueryCriteria.CommonQueryCriteria import net.corda.core.node.services.vault.QueryCriteria.CommonQueryCriteria
import net.corda.core.schemas.CommonSchemaV1
import net.corda.core.schemas.PersistentState import net.corda.core.schemas.PersistentState
import net.corda.core.schemas.PersistentStateRef import net.corda.core.schemas.PersistentStateRef
import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.toHexString
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor
import net.corda.core.utilities.toHexString
import net.corda.core.utilities.trace import net.corda.core.utilities.trace
import net.corda.core.schemas.CommonSchemaV1
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import java.util.* import java.util.*
import javax.persistence.Tuple import javax.persistence.Tuple

View File

@ -8,28 +8,32 @@ import io.requery.kotlin.eq
import io.requery.kotlin.notNull import io.requery.kotlin.notNull
import io.requery.query.RowExpression import io.requery.query.RowExpression
import net.corda.contracts.asset.Cash import net.corda.contracts.asset.Cash
import net.corda.contracts.asset.OnLedgerAsset
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.containsAny import net.corda.core.crypto.containsAny
import net.corda.core.crypto.toBase58String import net.corda.core.crypto.toBase58String
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party
import net.corda.core.internal.ThreadBox import net.corda.core.internal.ThreadBox
import net.corda.core.internal.tee import net.corda.core.internal.tee
import net.corda.core.node.ServiceHub import net.corda.core.node.ServiceHub
import net.corda.core.node.services.StatesNotAvailableException import net.corda.core.node.services.StatesNotAvailableException
import net.corda.core.node.services.Vault import net.corda.core.node.services.Vault
import net.corda.core.node.services.VaultService import net.corda.core.node.services.VaultService
import net.corda.core.node.services.vault.IQueryCriteriaParser
import net.corda.core.node.services.vault.QueryCriteria
import net.corda.core.node.services.vault.Sort
import net.corda.core.node.services.vault.SortAttribute
import net.corda.core.schemas.PersistentState
import net.corda.core.serialization.SerializationDefaults.STORAGE_CONTEXT import net.corda.core.serialization.SerializationDefaults.STORAGE_CONTEXT
import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.serialization.deserialize import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.core.transactions.CoreTransaction import net.corda.core.transactions.CoreTransaction
import net.corda.core.transactions.NotaryChangeWireTransaction import net.corda.core.transactions.NotaryChangeWireTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.transactions.WireTransaction import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.* import net.corda.core.utilities.NonEmptySet
import net.corda.core.utilities.loggerFor
import net.corda.core.utilities.toNonEmptySet
import net.corda.core.utilities.trace
import net.corda.node.services.database.RequeryConfiguration import net.corda.node.services.database.RequeryConfiguration
import net.corda.node.services.database.parserTransactionIsolationLevel import net.corda.node.services.database.parserTransactionIsolationLevel
import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.services.statemachine.FlowStateMachineImpl
@ -42,10 +46,8 @@ import net.corda.node.utilities.wrapWithDatabaseTransaction
import rx.Observable import rx.Observable
import rx.subjects.PublishSubject import rx.subjects.PublishSubject
import java.security.PublicKey import java.security.PublicKey
import java.sql.SQLException
import java.util.* import java.util.*
import java.util.concurrent.locks.ReentrantLock import javax.persistence.criteria.Predicate
import kotlin.concurrent.withLock
/** /**
* Currently, the node vault service is a very simple RDBMS backed implementation. It will change significantly when * Currently, the node vault service is a very simple RDBMS backed implementation. It will change significantly when
@ -79,6 +81,7 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P
// For use during publishing only. // For use during publishing only.
val updatesPublisher: rx.Observer<Vault.Update<ContractState>> get() = _updatesPublisher.bufferUntilDatabaseCommit().tee(_rawUpdatesPublisher) val updatesPublisher: rx.Observer<Vault.Update<ContractState>> get() = _updatesPublisher.bufferUntilDatabaseCommit().tee(_rawUpdatesPublisher)
} }
private val mutex = ThreadBox(InnerState()) private val mutex = ThreadBox(InnerState())
private fun recordUpdate(update: Vault.Update<ContractState>): Vault.Update<ContractState> { private fun recordUpdate(update: Vault.Update<ContractState>): Vault.Update<ContractState> {
@ -334,123 +337,110 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P
} }
} }
// coin selection retry loop counter, sleep (msecs) and lock for selecting states // TODO We shouldn't need to rewrite the query if we could modify the defaults.
val MAX_RETRIES = 5 private class QueryEditor<out T : ContractState>(val services: ServiceHub,
val RETRY_SLEEP = 100 val lockId: UUID,
val spendLock: ReentrantLock = ReentrantLock() val contractType: Class<out T>) : IQueryCriteriaParser {
var alreadyHasVaultQuery: Boolean = false
var modifiedCriteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(contractStateTypes = setOf(contractType),
softLockingCondition = QueryCriteria.SoftLockingCondition(QueryCriteria.SoftLockingType.UNLOCKED_AND_SPECIFIED, listOf(lockId)),
status = Vault.StateStatus.UNCONSUMED)
@Suspendable override fun parseCriteria(criteria: QueryCriteria.CommonQueryCriteria): Collection<Predicate> {
override fun <T : ContractState> unconsumedStatesForSpending(amount: Amount<Currency>, onlyFromIssuerParties: Set<AbstractParty>?, notary: Party?, lockId: UUID, withIssuerRefs: Set<OpaqueBytes>?): List<StateAndRef<T>> { modifiedCriteria = criteria
return emptyList()
val issuerKeysStr = onlyFromIssuerParties?.fold("") { left, right -> left + "('${right.owningKey.toBase58String()}')," }?.dropLast(1)
val issuerRefsStr = withIssuerRefs?.fold("") { left, right -> left + "('${right.bytes.toHexString()}')," }?.dropLast(1)
val stateAndRefs = mutableListOf<StateAndRef<T>>()
// TODO: Need to provide a database provider independent means of performing this function.
// We are using an H2 specific means of selecting a minimum set of rows that match a request amount of coins:
// 1) There is no standard SQL mechanism of calculating a cumulative total on a field and restricting row selection on the
// running total of such an accumulator
// 2) H2 uses session variables to perform this accumulator function:
// http://www.h2database.com/html/functions.html#set
// 3) H2 does not support JOIN's in FOR UPDATE (hence we are forced to execute 2 queries)
for (retryCount in 1..MAX_RETRIES) {
spendLock.withLock {
val statement = configuration.jdbcSession().createStatement()
try {
statement.execute("CALL SET(@t, 0);")
// we select spendable states irrespective of lock but prioritised by unlocked ones (Eg. null)
// the softLockReserve update will detect whether we try to lock states locked by others
val selectJoin = """
SELECT vs.transaction_id, vs.output_index, vs.contract_state, ccs.pennies, SET(@t, ifnull(@t,0)+ccs.pennies) total_pennies, vs.lock_id
FROM vault_states AS vs, contract_cash_states AS ccs
WHERE vs.transaction_id = ccs.transaction_id AND vs.output_index = ccs.output_index
AND vs.state_status = 0
AND ccs.ccy_code = '${amount.token}' and @t < ${amount.quantity}
AND (vs.lock_id = '$lockId' OR vs.lock_id is null)
""" +
(if (notary != null)
" AND vs.notary_key = '${notary.owningKey.toBase58String()}'" else "") +
(if (issuerKeysStr != null)
" 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)
stateAndRefs.clear()
log.debug(selectJoin)
var totalPennies = 0L
while (rs.next()) {
val txHash = SecureHash.parse(rs.getString(1))
val index = rs.getInt(2)
val stateRef = StateRef(txHash, index)
val state = rs.getBytes(3).deserialize<TransactionState<T>>(context = STORAGE_CONTEXT)
val pennies = rs.getLong(4)
totalPennies = rs.getLong(5)
val rowLockId = rs.getString(6)
stateAndRefs.add(StateAndRef(state, stateRef))
log.trace { "ROW: $rowLockId ($lockId): $stateRef : $pennies ($totalPennies)" }
}
if (stateAndRefs.isNotEmpty() && totalPennies >= amount.quantity) {
// we should have a minimum number of states to satisfy our selection `amount` criteria
log.trace("Coin selection for $amount retrieved ${stateAndRefs.count()} states totalling $totalPennies pennies: $stateAndRefs")
// update database
softLockReserve(lockId, (stateAndRefs.map { it.ref }).toNonEmptySet())
return stateAndRefs
}
log.trace("Coin selection requested $amount but retrieved $totalPennies pennies with state refs: ${stateAndRefs.map { it.ref }}")
// retry as more states may become available
} catch (e: SQLException) {
log.error("""Failed retrieving unconsumed states for: amount [$amount], onlyFromIssuerParties [$onlyFromIssuerParties], notary [$notary], lockId [$lockId]
$e.
""")
} catch (e: StatesNotAvailableException) {
stateAndRefs.clear()
log.warn(e.message)
// retry only if there are locked states that may become available again (or consumed with change)
} finally {
statement.close()
}
}
log.warn("Coin selection failed on attempt $retryCount")
// TODO: revisit the back off strategy for contended spending.
if (retryCount != MAX_RETRIES) {
FlowStateMachineImpl.sleep(RETRY_SLEEP * retryCount.toLong())
}
} }
log.warn("Insufficient spendable states identified for $amount") override fun parseCriteria(criteria: QueryCriteria.FungibleAssetQueryCriteria): Collection<Predicate> {
return stateAndRefs modifiedCriteria = criteria
return emptyList()
}
override fun parseCriteria(criteria: QueryCriteria.LinearStateQueryCriteria): Collection<Predicate> {
modifiedCriteria = criteria
return emptyList()
}
override fun <L : PersistentState> parseCriteria(criteria: QueryCriteria.VaultCustomQueryCriteria<L>): Collection<Predicate> {
modifiedCriteria = criteria
return emptyList()
}
override fun parseCriteria(criteria: QueryCriteria.VaultQueryCriteria): Collection<Predicate> {
modifiedCriteria = criteria.copy(contractStateTypes = setOf(contractType),
softLockingCondition = QueryCriteria.SoftLockingCondition(QueryCriteria.SoftLockingType.UNLOCKED_AND_SPECIFIED, listOf(lockId)),
status = Vault.StateStatus.UNCONSUMED)
alreadyHasVaultQuery = true
return emptyList()
}
override fun parseOr(left: QueryCriteria, right: QueryCriteria): Collection<Predicate> {
parse(left)
val modifiedLeft = modifiedCriteria
parse(right)
val modifiedRight = modifiedCriteria
modifiedCriteria = modifiedLeft.or(modifiedRight)
return emptyList()
}
override fun parseAnd(left: QueryCriteria, right: QueryCriteria): Collection<Predicate> {
parse(left)
val modifiedLeft = modifiedCriteria
parse(right)
val modifiedRight = modifiedCriteria
modifiedCriteria = modifiedLeft.and(modifiedRight)
return emptyList()
}
override fun parse(criteria: QueryCriteria, sorting: Sort?): Collection<Predicate> {
val basicQuery = modifiedCriteria
criteria.visit(this)
modifiedCriteria = if (alreadyHasVaultQuery) modifiedCriteria else criteria.and(basicQuery)
return emptyList()
}
fun queryForEligibleStates(criteria: QueryCriteria): Vault.Page<T> {
val sortAttribute = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF)
val sorter = Sort(setOf(Sort.SortColumn(sortAttribute, Sort.Direction.ASC)))
parse(criteria, sorter)
return services.vaultQueryService.queryBy(contractType, modifiedCriteria, sorter)
}
} }
/**
* Generate a transaction that moves an amount of currency to the given pubkey.
*
* @param onlyFromParties if non-null, the asset states will be filtered to only include those issued by the set
* of given parties. This can be useful if the party you're trying to pay has expectations
* about which type of asset claims they are willing to accept.
*/
@Suspendable @Suspendable
override fun generateSpend(tx: TransactionBuilder, @Throws(StatesNotAvailableException::class)
amount: Amount<Currency>, override fun <T : FungibleAsset<U>, U : Any> tryLockFungibleStatesForSpending(lockId: UUID,
to: AbstractParty, eligibleStatesQuery: QueryCriteria,
onlyFromParties: Set<AbstractParty>?): Pair<TransactionBuilder, List<PublicKey>> { amount: Amount<U>,
// Retrieve unspent and unlocked cash states that meet our spending criteria. contractType: Class<out T>): List<StateAndRef<T>> {
val acceptableCoins = unconsumedStatesForSpending<Cash.State>(amount, onlyFromParties, tx.notary, tx.lockId) if (amount.quantity == 0L) {
return OnLedgerAsset.generateSpend(tx, amount, to, acceptableCoins, return emptyList()
{ state, quantity, owner -> deriveState(state, quantity, owner) }, }
{ Cash().generateMoveCommand() })
}
private fun deriveState(txState: TransactionState<Cash.State>, amount: Amount<Issued<Currency>>, owner: AbstractParty) // TODO This helper code re-writes the query to alter the defaults on things such as soft locks
= txState.copy(data = txState.data.copy(amount = amount, owner = owner)) // and then runs the query. Ideally we would not need to do this.
val results = QueryEditor(services, lockId, contractType).queryForEligibleStates(eligibleStatesQuery)
var claimedAmount = 0L
val claimedStates = mutableListOf<StateAndRef<T>>()
for (state in results.states) {
val issuedAssetToken = state.state.data.amount.token
if (issuedAssetToken.product == amount.token) {
claimedStates += state
claimedAmount += state.state.data.amount.quantity
if (claimedAmount > amount.quantity) {
break
}
}
}
if (claimedStates.isEmpty() || claimedAmount < amount.quantity) {
return emptyList()
}
softLockReserve(lockId, claimedStates.map { it.ref }.toNonEmptySet())
return claimedStates
}
// TODO : Persists this in DB. // TODO : Persists this in DB.
private val authorisedUpgrade = mutableMapOf<StateRef, Class<out UpgradedContract<*, *>>>() private val authorisedUpgrade = mutableMapOf<StateRef, Class<out UpgradedContract<*, *>>>()

View File

@ -1,42 +1,50 @@
package net.corda.node.services.vault; package net.corda.node.services.vault;
import com.google.common.collect.*; import com.google.common.collect.ImmutableSet;
import net.corda.contracts.*; import kotlin.Pair;
import net.corda.contracts.asset.*; import net.corda.contracts.DealState;
import net.corda.contracts.asset.Cash;
import net.corda.core.contracts.*; import net.corda.core.contracts.*;
import net.corda.core.crypto.*; import net.corda.core.crypto.EncodingUtils;
import net.corda.core.identity.*; import net.corda.core.identity.AbstractParty;
import net.corda.core.messaging.*; import net.corda.core.messaging.DataFeed;
import net.corda.core.node.services.*; import net.corda.core.node.services.Vault;
import net.corda.core.node.services.VaultQueryException;
import net.corda.core.node.services.VaultQueryService;
import net.corda.core.node.services.VaultService;
import net.corda.core.node.services.vault.*; import net.corda.core.node.services.vault.*;
import net.corda.core.node.services.vault.QueryCriteria.*; import net.corda.core.node.services.vault.QueryCriteria.LinearStateQueryCriteria;
import net.corda.core.schemas.*; import net.corda.core.node.services.vault.QueryCriteria.VaultCustomQueryCriteria;
import net.corda.core.transactions.*; import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria;
import net.corda.core.utilities.*; import net.corda.core.utilities.OpaqueBytes;
import net.corda.node.services.database.*; import net.corda.node.utilities.CordaPersistence;
import net.corda.node.services.schema.*; import net.corda.schemas.CashSchemaV1;
import net.corda.node.utilities.*; import net.corda.testing.TestConstants;
import net.corda.schemas.*; import net.corda.testing.TestDependencyInjectionBase;
import net.corda.testing.*; import net.corda.testing.contracts.DummyLinearContract;
import net.corda.testing.contracts.*; import net.corda.testing.contracts.VaultFiller;
import net.corda.testing.node.*; import net.corda.testing.node.MockServices;
import net.corda.testing.schemas.*; import org.junit.After;
import org.jetbrains.annotations.*; import org.junit.Before;
import org.junit.*; import org.junit.Test;
import rx.Observable; import rx.Observable;
import java.io.*; import java.io.IOException;
import java.lang.reflect.*; import java.lang.reflect.Field;
import java.security.KeyPair;
import java.util.*; import java.util.*;
import java.util.stream.*; import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import static net.corda.contracts.asset.CashKt.*; import static net.corda.contracts.asset.CashKt.getDUMMY_CASH_ISSUER;
import static net.corda.core.node.services.vault.QueryCriteriaUtils.*; import static net.corda.contracts.asset.CashKt.getDUMMY_CASH_ISSUER_KEY;
import static net.corda.core.utilities.ByteArrays.*; import static net.corda.core.node.services.vault.QueryCriteriaUtils.DEFAULT_PAGE_NUM;
import static net.corda.node.utilities.CordaPersistenceKt.*; import static net.corda.core.node.services.vault.QueryCriteriaUtils.MAX_PAGE_SIZE;
import static net.corda.core.utilities.ByteArrays.toHexString;
import static net.corda.testing.CoreTestUtils.*; import static net.corda.testing.CoreTestUtils.*;
import static net.corda.testing.node.MockServicesKt.*; import static net.corda.testing.node.MockServicesKt.makeTestDatabaseAndMockServices;
import static org.assertj.core.api.Assertions.*; import static org.assertj.core.api.Assertions.assertThat;
public class VaultQueryJavaTests extends TestDependencyInjectionBase { public class VaultQueryJavaTests extends TestDependencyInjectionBase {
@ -47,38 +55,13 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase {
@Before @Before
public void setUp() { public void setUp() {
Properties dataSourceProps = makeTestDataSourceProperties(SecureHash.randomSHA256().toString()); ArrayList<KeyPair> keys = new ArrayList<>();
database = configureDatabase(dataSourceProps, makeTestDatabaseProperties()); keys.add(getMEGA_CORP_KEY());
database.transaction(statement -> { Pair<CordaPersistence, MockServices> databaseAndServices = makeTestDatabaseAndMockServices(Collections.EMPTY_SET, keys);
Set<MappedSchema> customSchemas = new HashSet<>(Collections.singletonList(DummyLinearStateSchemaV1.INSTANCE)); database = databaseAndServices.getFirst();
HibernateConfiguration hibernateConfig = new HibernateConfiguration(new NodeSchemaService(customSchemas), makeTestDatabaseProperties()); services = databaseAndServices.getSecond();
services = new MockServices(getMEGA_CORP_KEY()) { vaultSvc = services.getVaultService();
@NotNull vaultQuerySvc = services.getVaultQueryService();
@Override
public VaultService getVaultService() {
if (vaultSvc != null) return vaultSvc;
return makeVaultService(dataSourceProps, hibernateConfig);
}
@NotNull
@Override
public VaultQueryService getVaultQueryService() {
return new HibernateVaultQueryImpl(hibernateConfig, vaultSvc.getUpdatesPublisher());
}
@Override
public void recordTransactions(@NotNull Iterable<SignedTransaction> txs) {
for (SignedTransaction stx : txs) {
getValidatedTransactions().addTransaction(stx);
}
Stream<WireTransaction> wtxn = StreamSupport.stream(txs.spliterator(), false).map(SignedTransaction::getTx);
vaultSvc.notifyAll(wtxn.collect(Collectors.toList()));
}
};
vaultSvc = services.getVaultService();
vaultQuerySvc = services.getVaultQueryService();
return services;
});
} }
@After @After

View File

@ -148,8 +148,8 @@ class TwoPartyTradeFlowTests {
bobNode.disableDBCloseOnStop() bobNode.disableDBCloseOnStop()
val cashStates = bobNode.database.transaction { val cashStates = bobNode.database.transaction {
bobNode.services.fillWithSomeTestCash(2000.DOLLARS, notaryNode.info.notaryIdentity, 3, 3) bobNode.services.fillWithSomeTestCash(2000.DOLLARS, notaryNode.info.notaryIdentity, 3, 3)
} }
val alicesFakePaper = aliceNode.database.transaction { val alicesFakePaper = aliceNode.database.transaction {
fillUpForSeller(false, cpIssuer, aliceNode.info.legalIdentity, fillUpForSeller(false, cpIssuer, aliceNode.info.legalIdentity,
@ -168,7 +168,7 @@ class TwoPartyTradeFlowTests {
} }
val (bobStateMachine, aliceResult) = runBuyerAndSeller(notaryNode, aliceNode, bobNode, val (bobStateMachine, aliceResult) = runBuyerAndSeller(notaryNode, aliceNode, bobNode,
"alice's paper".outputStateAndRef()) "alice's paper".outputStateAndRef())
assertEquals(aliceResult.getOrThrow(), bobStateMachine.getOrThrow().resultFuture.getOrThrow()) assertEquals(aliceResult.getOrThrow(), bobStateMachine.getOrThrow().resultFuture.getOrThrow())
@ -533,11 +533,11 @@ class TwoPartyTradeFlowTests {
override fun call(): SignedTransaction { override fun call(): SignedTransaction {
send(buyer, Pair(notary.notaryIdentity, price)) send(buyer, Pair(notary.notaryIdentity, price))
return subFlow(Seller( return subFlow(Seller(
buyer, buyer,
notary, notary,
assetToSell, assetToSell,
price, price,
me.party)) me.party))
} }
} }

View File

@ -3,10 +3,12 @@ package net.corda.node.services.database
import net.corda.contracts.asset.Cash import net.corda.contracts.asset.Cash
import net.corda.contracts.asset.DUMMY_CASH_ISSUER import net.corda.contracts.asset.DUMMY_CASH_ISSUER
import net.corda.contracts.asset.DummyFungibleContract import net.corda.contracts.asset.DummyFungibleContract
import net.corda.contracts.asset.sumCash
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.toBase58String import net.corda.core.crypto.toBase58String
import net.corda.core.node.services.Vault import net.corda.core.node.services.Vault
import net.corda.core.node.services.VaultQueryService
import net.corda.core.node.services.VaultService import net.corda.core.node.services.VaultService
import net.corda.core.schemas.CommonSchemaV1 import net.corda.core.schemas.CommonSchemaV1
import net.corda.core.schemas.PersistentStateRef import net.corda.core.schemas.PersistentStateRef
@ -14,6 +16,7 @@ import net.corda.core.serialization.deserialize
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.node.services.schema.HibernateObserver import net.corda.node.services.schema.HibernateObserver
import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.schema.NodeSchemaService
import net.corda.node.services.vault.HibernateVaultQueryImpl
import net.corda.node.services.vault.NodeVaultService import net.corda.node.services.vault.NodeVaultService
import net.corda.node.services.vault.VaultSchemaV1 import net.corda.node.services.vault.VaultSchemaV1
import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.CordaPersistence
@ -38,6 +41,7 @@ import org.junit.After
import org.junit.Assert import org.junit.Assert
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import java.math.BigDecimal
import java.time.Instant import java.time.Instant
import java.util.* import java.util.*
import javax.persistence.EntityManager import javax.persistence.EntityManager
@ -126,7 +130,8 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() {
// execute query // execute query
val queryResults = entityManager.createQuery(criteriaQuery).resultList val queryResults = entityManager.createQuery(criteriaQuery).resultList
assertThat(queryResults.size).isEqualTo(6) val coins = queryResults.map { it.contractState.deserialize<TransactionState<Cash.State>>().data }.sumCash()
assertThat(coins.toDecimal() >= BigDecimal("50.00"))
} }
@Test @Test

View File

@ -1,39 +1,40 @@
package net.corda.node.services.vault package net.corda.node.services.vault
import co.paralleluniverse.fibers.Suspendable
import net.corda.contracts.asset.Cash import net.corda.contracts.asset.Cash
import net.corda.contracts.asset.DUMMY_CASH_ISSUER import net.corda.contracts.asset.DUMMY_CASH_ISSUER
import net.corda.contracts.asset.sumCash
import net.corda.contracts.getCashBalance import net.corda.contracts.getCashBalance
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.generateKeyPair import net.corda.core.crypto.generateKeyPair
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party
import net.corda.core.node.services.StatesNotAvailableException import net.corda.core.node.services.StatesNotAvailableException
import net.corda.core.node.services.Vault import net.corda.core.node.services.Vault
import net.corda.core.node.services.VaultQueryService import net.corda.core.node.services.VaultQueryService
import net.corda.core.node.services.VaultService import net.corda.core.node.services.VaultService
import net.corda.core.node.services.queryBy import net.corda.core.node.services.queryBy
import net.corda.core.node.services.vault.QueryCriteria
import net.corda.core.node.services.vault.QueryCriteria.* import net.corda.core.node.services.vault.QueryCriteria.*
import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria
import net.corda.core.transactions.NotaryChangeWireTransaction import net.corda.core.transactions.NotaryChangeWireTransaction
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.NonEmptySet import net.corda.core.utilities.NonEmptySet
import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.toNonEmptySet import net.corda.core.utilities.toNonEmptySet
import net.corda.node.services.database.HibernateConfiguration
import net.corda.node.services.schema.NodeSchemaService
import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.CordaPersistence
import net.corda.node.utilities.configureDatabase
import net.corda.testing.* import net.corda.testing.*
import net.corda.testing.contracts.fillWithSomeTestCash import net.corda.testing.contracts.fillWithSomeTestCash
import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices
import net.corda.testing.node.makeTestDataSourceProperties import net.corda.testing.node.makeTestDatabaseAndMockServices
import net.corda.testing.node.makeTestDatabaseProperties
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.assertj.core.api.Assertions.assertThatExceptionOfType
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import rx.observers.TestSubscriber import rx.observers.TestSubscriber
import java.math.BigDecimal
import java.util.* import java.util.*
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executors import java.util.concurrent.Executors
@ -50,23 +51,9 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
@Before @Before
fun setUp() { fun setUp() {
LogHelper.setLevel(NodeVaultService::class) LogHelper.setLevel(NodeVaultService::class)
val dataSourceProps = makeTestDataSourceProperties() val databaseAndServices = makeTestDatabaseAndMockServices()
database = configureDatabase(dataSourceProps, makeTestDatabaseProperties()) database = databaseAndServices.first
database.transaction { services = databaseAndServices.second
val hibernateConfig = HibernateConfiguration(NodeSchemaService(), makeTestDatabaseProperties())
services = object : MockServices() {
override val vaultService: VaultService = makeVaultService(dataSourceProps, hibernateConfig)
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
for (stx in txs) {
validatedTransactions.addTransaction(stx)
}
// Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions.
vaultService.notifyAll(txs.map { it.tx })
}
override val vaultQueryService : VaultQueryService = HibernateVaultQueryImpl(hibernateConfig, vaultService.updatesPublisher)
}
}
} }
@After @After
@ -75,6 +62,25 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
LogHelper.reset(NodeVaultService::class) LogHelper.reset(NodeVaultService::class)
} }
@Suspendable
private fun VaultService.unconsumedCashStatesForSpending(amount: Amount<Currency>,
onlyFromIssuerParties: Set<AbstractParty>? = null,
notary: Party? = null,
lockId: UUID = UUID.randomUUID(),
withIssuerRefs: Set<OpaqueBytes>? = null): List<StateAndRef<Cash.State>> {
val notaryName = if (notary != null) listOf(notary.name) else null
var baseCriteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(notaryName = notaryName)
if (onlyFromIssuerParties != null || withIssuerRefs != null) {
baseCriteria = baseCriteria.and(QueryCriteria.FungibleAssetQueryCriteria(
issuerPartyName = onlyFromIssuerParties?.toList(),
issuerRef = withIssuerRefs?.toList()))
}
return tryLockFungibleStatesForSpending(lockId, baseCriteria, amount, Cash.State::class.java)
}
@Test @Test
fun `states not local to instance`() { fun `states not local to instance`() {
database.transaction { database.transaction {
@ -308,7 +314,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
val unconsumedStates = vaultQuery.queryBy<Cash.State>().states val unconsumedStates = vaultQuery.queryBy<Cash.State>().states
assertThat(unconsumedStates).hasSize(1) assertThat(unconsumedStates).hasSize(1)
val spendableStatesUSD = (vaultSvc as NodeVaultService).unconsumedStatesForSpending<Cash.State>(100.DOLLARS, lockId = UUID.randomUUID()) val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(100.DOLLARS)
spendableStatesUSD.forEach(::println) spendableStatesUSD.forEach(::println)
assertThat(spendableStatesUSD).hasSize(1) assertThat(spendableStatesUSD).hasSize(1)
assertThat(spendableStatesUSD[0].state.data.amount.quantity).isEqualTo(100L * 100) assertThat(spendableStatesUSD[0].state.data.amount.quantity).isEqualTo(100L * 100)
@ -324,12 +330,13 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
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 = (DUMMY_CASH_ISSUER))
services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(1)), issuerKey = BOC_KEY) services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(1)), issuerKey = BOC_KEY)
val spendableStatesUSD = vaultSvc.unconsumedStatesForSpending<Cash.State>(200.DOLLARS, lockId = UUID.randomUUID(), val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(200.DOLLARS,
onlyFromIssuerParties = setOf(DUMMY_CASH_ISSUER.party, BOC)).toList() onlyFromIssuerParties = setOf(DUMMY_CASH_ISSUER.party, BOC))
spendableStatesUSD.forEach(::println) spendableStatesUSD.forEach(::println)
assertThat(spendableStatesUSD).hasSize(2) assertThat(spendableStatesUSD).hasSize(2)
assertThat(spendableStatesUSD[0].state.data.amount.token.issuer).isEqualTo(DUMMY_CASH_ISSUER) assertThat(spendableStatesUSD[0].state.data.amount.token.issuer).isIn(DUMMY_CASH_ISSUER, BOC.ref(1))
assertThat(spendableStatesUSD[1].state.data.amount.token.issuer).isEqualTo(BOC.ref(1)) assertThat(spendableStatesUSD[1].state.data.amount.token.issuer).isIn(DUMMY_CASH_ISSUER, BOC.ref(1))
assertThat(spendableStatesUSD[0].state.data.amount.token.issuer).isNotEqualTo(spendableStatesUSD[1].state.data.amount.token.issuer)
} }
} }
@ -345,12 +352,13 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
val unconsumedStates = vaultQuery.queryBy<Cash.State>().states val unconsumedStates = vaultQuery.queryBy<Cash.State>().states
assertThat(unconsumedStates).hasSize(4) assertThat(unconsumedStates).hasSize(4)
val spendableStatesUSD = vaultSvc.unconsumedStatesForSpending<Cash.State>(200.DOLLARS, lockId = UUID.randomUUID(), val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(200.DOLLARS,
onlyFromIssuerParties = setOf(BOC), withIssuerRefs = setOf(OpaqueBytes.of(1), OpaqueBytes.of(2))).toList() onlyFromIssuerParties = setOf(BOC), withIssuerRefs = setOf(OpaqueBytes.of(1), OpaqueBytes.of(2)))
assertThat(spendableStatesUSD).hasSize(2) assertThat(spendableStatesUSD).hasSize(2)
assertThat(spendableStatesUSD[0].state.data.amount.token.issuer.party).isEqualTo(BOC) 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[0].state.data.amount.token.issuer.reference).isIn(BOC.ref(1).reference, BOC.ref(2).reference)
assertThat(spendableStatesUSD[1].state.data.amount.token.issuer.reference).isEqualTo(BOC.ref(2).reference) assertThat(spendableStatesUSD[1].state.data.amount.token.issuer.reference).isIn(BOC.ref(1).reference, BOC.ref(2).reference)
assertThat(spendableStatesUSD[0].state.data.amount.token.issuer.reference).isNotEqualTo(spendableStatesUSD[1].state.data.amount.token.issuer.reference)
} }
} }
@ -363,9 +371,9 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
val unconsumedStates = vaultQuery.queryBy<Cash.State>().states val unconsumedStates = vaultQuery.queryBy<Cash.State>().states
assertThat(unconsumedStates).hasSize(1) assertThat(unconsumedStates).hasSize(1)
val spendableStatesUSD = (vaultSvc as NodeVaultService).unconsumedStatesForSpending<Cash.State>(110.DOLLARS, lockId = UUID.randomUUID()) val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(110.DOLLARS)
spendableStatesUSD.forEach(::println) spendableStatesUSD.forEach(::println)
assertThat(spendableStatesUSD).hasSize(1) assertThat(spendableStatesUSD).hasSize(0)
val criteriaLocked = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.LOCKED_ONLY)) val criteriaLocked = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.LOCKED_ONLY))
assertThat(vaultQuery.queryBy<Cash.State>(criteriaLocked).states).hasSize(0) assertThat(vaultQuery.queryBy<Cash.State>(criteriaLocked).states).hasSize(0)
} }
@ -380,7 +388,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
val unconsumedStates = vaultQuery.queryBy<Cash.State>().states val unconsumedStates = vaultQuery.queryBy<Cash.State>().states
assertThat(unconsumedStates).hasSize(2) assertThat(unconsumedStates).hasSize(2)
val spendableStatesUSD = (vaultSvc as NodeVaultService).unconsumedStatesForSpending<Cash.State>(1.DOLLARS, lockId = UUID.randomUUID()) val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(1.DOLLARS)
spendableStatesUSD.forEach(::println) spendableStatesUSD.forEach(::println)
assertThat(spendableStatesUSD).hasSize(1) assertThat(spendableStatesUSD).hasSize(1)
assertThat(spendableStatesUSD[0].state.data.amount.quantity).isGreaterThanOrEqualTo(100L) assertThat(spendableStatesUSD[0].state.data.amount.quantity).isGreaterThanOrEqualTo(100L)
@ -397,16 +405,30 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
services.fillWithSomeTestCash(100.POUNDS, DUMMY_NOTARY, 10, 10, Random(0L)) services.fillWithSomeTestCash(100.POUNDS, DUMMY_NOTARY, 10, 10, Random(0L))
services.fillWithSomeTestCash(100.SWISS_FRANCS, DUMMY_NOTARY, 10, 10, Random(0L)) services.fillWithSomeTestCash(100.SWISS_FRANCS, DUMMY_NOTARY, 10, 10, Random(0L))
var unlockedStates = 30
val allStates = vaultQuery.queryBy<Cash.State>().states val allStates = vaultQuery.queryBy<Cash.State>().states
assertThat(allStates).hasSize(30) assertThat(allStates).hasSize(unlockedStates)
var lockedCount = 0
for (i in 1..5) { for (i in 1..5) {
val spendableStatesUSD = (vaultSvc as NodeVaultService).unconsumedStatesForSpending<Cash.State>(20.DOLLARS, lockId = UUID.randomUUID()) val lockId = UUID.randomUUID()
val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(20.DOLLARS, lockId = lockId)
spendableStatesUSD.forEach(::println) spendableStatesUSD.forEach(::println)
assertThat(spendableStatesUSD.size <= unlockedStates)
unlockedStates -= spendableStatesUSD.size
val criteriaLocked = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.SPECIFIED, listOf(lockId)))
val lockedStates = vaultQuery.queryBy<Cash.State>(criteriaLocked).states
if (spendableStatesUSD.isNotEmpty()) {
assertEquals(spendableStatesUSD.size, lockedStates.size)
val lockedTotal = lockedStates.map { it.state.data }.sumCash()
val foundAmount = spendableStatesUSD.map { it.state.data }.sumCash()
assertThat(foundAmount.toDecimal() >= BigDecimal("20.00"))
assertThat(lockedTotal == foundAmount)
lockedCount += lockedStates.size
}
} }
// note only 3 spend attempts succeed with a total of 8 states
val criteriaLocked = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.LOCKED_ONLY)) val criteriaLocked = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.LOCKED_ONLY))
assertThat(vaultQuery.queryBy<Cash.State>(criteriaLocked).states).hasSize(8) assertThat(vaultQuery.queryBy<Cash.State>(criteriaLocked).states).hasSize(lockedCount)
} }
} }
@ -484,7 +506,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
database.transaction { database.transaction {
val moveTx = TransactionBuilder(services.myInfo.legalIdentity).apply { val moveTx = TransactionBuilder(services.myInfo.legalIdentity).apply {
service.generateSpend(this, Amount(1000, GBP), thirdPartyIdentity) Cash.generateSpend(services, this, Amount(1000, GBP), thirdPartyIdentity)
}.toWireTransaction() }.toWireTransaction()
service.notify(moveTx) service.notify(moveTx)
} }
@ -530,7 +552,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
// Move cash // Move cash
val moveTx = database.transaction { val moveTx = database.transaction {
TransactionBuilder(newNotary).apply { TransactionBuilder(newNotary).apply {
service.generateSpend(this, Amount(1000, GBP), thirdPartyIdentity) Cash.generateSpend(services, this, Amount(1000, GBP), thirdPartyIdentity)
}.toWireTransaction() }.toWireTransaction()
} }

View File

@ -1,23 +1,19 @@
package net.corda.node.services.vault package net.corda.node.services.vault
import net.corda.contracts.* import net.corda.contracts.CommercialPaper
import net.corda.contracts.Commodity
import net.corda.contracts.DealState
import net.corda.contracts.asset.Cash import net.corda.contracts.asset.Cash
import net.corda.contracts.asset.DUMMY_CASH_ISSUER import net.corda.contracts.asset.DUMMY_CASH_ISSUER
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.entropyToKeyPair import net.corda.core.crypto.entropyToKeyPair
import net.corda.core.crypto.toBase58String import net.corda.core.crypto.toBase58String
import net.corda.core.utilities.days
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.node.services.* import net.corda.core.node.services.*
import net.corda.core.node.services.vault.* import net.corda.core.node.services.vault.*
import net.corda.core.node.services.vault.QueryCriteria.* import net.corda.core.node.services.vault.QueryCriteria.*
import net.corda.core.utilities.seconds import net.corda.core.utilities.*
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.NonEmptySet
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.toHexString
import net.corda.node.services.database.HibernateConfiguration
import net.corda.node.services.schema.NodeSchemaService
import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.CordaPersistence
import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.configureDatabase
import net.corda.schemas.CashSchemaV1 import net.corda.schemas.CashSchemaV1
@ -27,7 +23,7 @@ import net.corda.schemas.SampleCashSchemaV3
import net.corda.testing.* import net.corda.testing.*
import net.corda.testing.contracts.* import net.corda.testing.contracts.*
import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices
import net.corda.testing.node.makeTestDataSourceProperties import net.corda.testing.node.makeTestDatabaseAndMockServices
import net.corda.testing.node.makeTestDatabaseProperties import net.corda.testing.node.makeTestDatabaseProperties
import net.corda.testing.schemas.DummyLinearStateSchemaV1 import net.corda.testing.schemas.DummyLinearStateSchemaV1
import org.assertj.core.api.Assertions import org.assertj.core.api.Assertions
@ -54,24 +50,9 @@ class VaultQueryTests : TestDependencyInjectionBase() {
@Before @Before
fun setUp() { fun setUp() {
val dataSourceProps = makeTestDataSourceProperties() val databaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(MEGA_CORP_KEY))
database = configureDatabase(dataSourceProps, makeTestDatabaseProperties()) database = databaseAndServices.first
database.transaction { services = databaseAndServices.second
val customSchemas = setOf(CommercialPaperSchemaV1, DummyLinearStateSchemaV1)
val hibernateConfig = HibernateConfiguration(NodeSchemaService(customSchemas), makeTestDatabaseProperties())
services = object : MockServices(MEGA_CORP_KEY) {
override val vaultService: VaultService = makeVaultService(dataSourceProps, hibernateConfig)
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
for (stx in txs) {
validatedTransactions.addTransaction(stx)
}
// Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions.
vaultService.notifyAll(txs.map { it.tx })
}
override val vaultQueryService : VaultQueryService = HibernateVaultQueryImpl(hibernateConfig, vaultService.updatesPublisher)
}
}
} }
@After @After
@ -97,7 +78,7 @@ class VaultQueryTests : TestDependencyInjectionBase() {
_database.transaction { _database.transaction {
// create new states // create new states
services.fillWithSomeTestCash(100.DOLLARS, CASH_NOTARY, 10, 10, Random(0L)) services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 10, 10, Random(0L))
val linearStatesXYZ = services.fillWithSomeTestLinearStates(1, "XYZ") val linearStatesXYZ = services.fillWithSomeTestLinearStates(1, "XYZ")
val linearStatesJKL = services.fillWithSomeTestLinearStates(2, "JKL") val linearStatesJKL = services.fillWithSomeTestLinearStates(2, "JKL")
services.fillWithSomeTestLinearStates(3, "ABC") services.fillWithSomeTestLinearStates(3, "ABC")
@ -239,15 +220,15 @@ class VaultQueryTests : TestDependencyInjectionBase() {
fun `unconsumed cash states sorted by state ref`() { fun `unconsumed cash states sorted by state ref`() {
database.transaction { database.transaction {
var stateRefs : MutableList<StateRef> = mutableListOf() val stateRefs: MutableList<StateRef> = mutableListOf()
val issuedStates = services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 10, 10, Random(0L)) val issuedStates = services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 10, 10, Random(0L))
val issuedStateRefs = issuedStates.states.map { it.ref }.toList() val issuedStateRefs = issuedStates.states.map { it.ref }.toList()
stateRefs.addAll(issuedStateRefs) stateRefs.addAll(issuedStateRefs)
val spentStates = services.consumeCash(25.DOLLARS) val spentStates = services.consumeCash(25.DOLLARS)
var consumedStateRefs = spentStates.consumed.map { it.ref }.toList() val consumedStateRefs = spentStates.consumed.map { it.ref }.toList()
var producedStateRefs = spentStates.produced.map { it.ref }.toList() val producedStateRefs = spentStates.produced.map { it.ref }.toList()
stateRefs.addAll(consumedStateRefs.plus(producedStateRefs)) stateRefs.addAll(consumedStateRefs.plus(producedStateRefs))
val sortAttribute = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF) val sortAttribute = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF)
@ -271,8 +252,9 @@ class VaultQueryTests : TestDependencyInjectionBase() {
fun `unconsumed cash states sorted by state ref txnId and index`() { fun `unconsumed cash states sorted by state ref txnId and index`() {
database.transaction { database.transaction {
services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 10, 10, Random(0L)) services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 10, 10, Random(0L))
services.consumeCash(10.DOLLARS) val consumed = mutableSetOf<SecureHash>()
services.consumeCash(10.DOLLARS) services.consumeCash(10.DOLLARS).consumed.forEach { consumed += it.ref.txhash }
services.consumeCash(10.DOLLARS).consumed.forEach { consumed += it.ref.txhash }
val sortAttributeTxnId = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF_TXN_ID) val sortAttributeTxnId = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF_TXN_ID)
val sortAttributeIndex = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF_INDEX) val sortAttributeIndex = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF_INDEX)
@ -283,13 +265,11 @@ class VaultQueryTests : TestDependencyInjectionBase() {
results.statesMetadata.forEach { results.statesMetadata.forEach {
println(" ${it.ref}") println(" ${it.ref}")
assertThat(it.status).isEqualTo(Vault.StateStatus.UNCONSUMED)
} }
val sorted = results.states.sortedBy { it.ref.toString() }
// explicit sort order asc by txnId and then index: assertThat(results.states).isEqualTo(sorted)
// order by assertThat(results.states).allSatisfy { !consumed.contains(it.ref.txhash) }
// vaultschem1_.transaction_id asc,
// vaultschem1_.output_index asc
assertThat(results.states).hasSize(9) // -2 CONSUMED + 1 NEW UNCONSUMED (change)
} }
} }
@ -411,7 +391,7 @@ class VaultQueryTests : TestDependencyInjectionBase() {
} }
} }
val CASH_NOTARY_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(20)) } val CASH_NOTARY_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(21)) }
val CASH_NOTARY: Party get() = Party(X500Name("CN=Cash Notary Service,O=R3,OU=corda,L=Zurich,C=CH"), CASH_NOTARY_KEY.public) val CASH_NOTARY: Party get() = Party(X500Name("CN=Cash Notary Service,O=R3,OU=corda,L=Zurich,C=CH"), CASH_NOTARY_KEY.public)
@Test @Test
@ -870,7 +850,7 @@ class VaultQueryTests : TestDependencyInjectionBase() {
fun `aggregate functions count by contract type and state status`() { fun `aggregate functions count by contract type and state status`() {
database.transaction { database.transaction {
// create new states // create new states
services.fillWithSomeTestCash(100.DOLLARS, CASH_NOTARY, 10, 10, Random(0L)) services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 10, 10, Random(0L))
val linearStatesXYZ = services.fillWithSomeTestLinearStates(1, "XYZ") val linearStatesXYZ = services.fillWithSomeTestLinearStates(1, "XYZ")
val linearStatesJKL = services.fillWithSomeTestLinearStates(2, "JKL") val linearStatesJKL = services.fillWithSomeTestLinearStates(2, "JKL")
services.fillWithSomeTestLinearStates(3, "ABC") services.fillWithSomeTestLinearStates(3, "ABC")
@ -896,14 +876,14 @@ class VaultQueryTests : TestDependencyInjectionBase() {
services.consumeLinearStates(linearStatesXYZ.states.toList()) services.consumeLinearStates(linearStatesXYZ.states.toList())
services.consumeLinearStates(linearStatesJKL.states.toList()) services.consumeLinearStates(linearStatesJKL.states.toList())
services.consumeDeals(dealStates.states.filter { it.state.data.ref == "456" }) services.consumeDeals(dealStates.states.filter { it.state.data.ref == "456" })
services.consumeCash(50.DOLLARS) val cashUpdates = services.consumeCash(50.DOLLARS)
// UNCONSUMED states (default) // UNCONSUMED states (default)
// count fungible assets // count fungible assets
val countCriteriaUnconsumed = QueryCriteria.VaultCustomQueryCriteria(count, Vault.StateStatus.UNCONSUMED) val countCriteriaUnconsumed = QueryCriteria.VaultCustomQueryCriteria(count, Vault.StateStatus.UNCONSUMED)
val fungibleStateCountUnconsumed = vaultQuerySvc.queryBy<FungibleAsset<*>>(countCriteriaUnconsumed).otherResults.single() as Long val fungibleStateCountUnconsumed = vaultQuerySvc.queryBy<FungibleAsset<*>>(countCriteriaUnconsumed).otherResults.single() as Long
assertThat(fungibleStateCountUnconsumed).isEqualTo(5L) assertThat(fungibleStateCountUnconsumed.toInt()).isEqualTo(10 - cashUpdates.consumed.size + cashUpdates.produced.size)
// count linear states // count linear states
val linearStateCountUnconsumed = vaultQuerySvc.queryBy<LinearState>(countCriteriaUnconsumed).otherResults.single() as Long val linearStateCountUnconsumed = vaultQuerySvc.queryBy<LinearState>(countCriteriaUnconsumed).otherResults.single() as Long
@ -918,7 +898,7 @@ class VaultQueryTests : TestDependencyInjectionBase() {
// count fungible assets // count fungible assets
val countCriteriaConsumed = QueryCriteria.VaultCustomQueryCriteria(count, Vault.StateStatus.CONSUMED) val countCriteriaConsumed = QueryCriteria.VaultCustomQueryCriteria(count, Vault.StateStatus.CONSUMED)
val fungibleStateCountConsumed = vaultQuerySvc.queryBy<FungibleAsset<*>>(countCriteriaConsumed).otherResults.single() as Long val fungibleStateCountConsumed = vaultQuerySvc.queryBy<FungibleAsset<*>>(countCriteriaConsumed).otherResults.single() as Long
assertThat(fungibleStateCountConsumed).isEqualTo(6L) assertThat(fungibleStateCountConsumed.toInt()).isEqualTo(cashUpdates.consumed.size)
// count linear states // count linear states
val linearStateCountConsumed = vaultQuerySvc.queryBy<LinearState>(countCriteriaConsumed).otherResults.single() as Long val linearStateCountConsumed = vaultQuerySvc.queryBy<LinearState>(countCriteriaConsumed).otherResults.single() as Long
@ -962,7 +942,7 @@ class VaultQueryTests : TestDependencyInjectionBase() {
fun `states consumed after time`() { fun `states consumed after time`() {
database.transaction { database.transaction {
services.fillWithSomeTestCash(100.DOLLARS, CASH_NOTARY, 3, 3, Random(0L)) services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L))
services.fillWithSomeTestLinearStates(10) services.fillWithSomeTestLinearStates(10)
services.fillWithSomeTestDeals(listOf("123", "456", "789")) services.fillWithSomeTestDeals(listOf("123", "456", "789"))

View File

@ -11,17 +11,12 @@ import net.corda.core.node.services.VaultService
import net.corda.core.node.services.queryBy import net.corda.core.node.services.queryBy
import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.node.services.vault.QueryCriteria
import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.node.services.database.HibernateConfiguration
import net.corda.node.services.schema.NodeSchemaService
import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.CordaPersistence
import net.corda.node.utilities.configureDatabase
import net.corda.testing.* import net.corda.testing.*
import net.corda.testing.contracts.* import net.corda.testing.contracts.*
import net.corda.testing.node.MockServices import net.corda.testing.node.MockServices
import net.corda.testing.node.makeTestDataSourceProperties import net.corda.testing.node.makeTestDatabaseAndMockServices
import net.corda.testing.node.makeTestDatabaseProperties
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.After import org.junit.After
@ -44,23 +39,9 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
@Before @Before
fun setUp() { fun setUp() {
LogHelper.setLevel(VaultWithCashTest::class) LogHelper.setLevel(VaultWithCashTest::class)
val dataSourceProps = makeTestDataSourceProperties() val databaseAndServices = makeTestDatabaseAndMockServices()
database = configureDatabase(dataSourceProps, makeTestDatabaseProperties()) database = databaseAndServices.first
database.transaction { services = databaseAndServices.second
val hibernateConfig = HibernateConfiguration(NodeSchemaService(), makeTestDatabaseProperties())
services = object : MockServices() {
override val vaultService: VaultService = makeVaultService(dataSourceProps, hibernateConfig)
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
for (stx in txs) {
validatedTransactions.addTransaction(stx)
}
// Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions.
vaultService.notifyAll(txs.map { it.tx })
}
override val vaultQueryService : VaultQueryService = HibernateVaultQueryImpl(hibernateConfig, vaultService.updatesPublisher)
}
}
} }
@After @After
@ -103,7 +84,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
// A tx that spends our money. // A tx that spends our money.
val spendTXBuilder = TransactionBuilder(DUMMY_NOTARY) val spendTXBuilder = TransactionBuilder(DUMMY_NOTARY)
vault.generateSpend(spendTXBuilder, 80.DOLLARS, BOB) Cash.generateSpend(services, spendTXBuilder, 80.DOLLARS, BOB)
val spendPTX = services.signInitialTransaction(spendTXBuilder, freshKey) val spendPTX = services.signInitialTransaction(spendTXBuilder, freshKey)
val spendTX = notaryServices.addSignature(spendPTX) val spendTX = notaryServices.addSignature(spendPTX)
@ -151,7 +132,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
database.transaction { database.transaction {
try { try {
val txn1Builder = TransactionBuilder(DUMMY_NOTARY) val txn1Builder = TransactionBuilder(DUMMY_NOTARY)
vault.generateSpend(txn1Builder, 60.DOLLARS, BOB) Cash.generateSpend(services, txn1Builder, 60.DOLLARS, BOB)
val ptxn1 = notaryServices.signInitialTransaction(txn1Builder) val ptxn1 = notaryServices.signInitialTransaction(txn1Builder)
val txn1 = services.addSignature(ptxn1, freshKey) val txn1 = services.addSignature(ptxn1, freshKey)
println("txn1: ${txn1.id} spent ${((txn1.tx.outputs[0].data) as Cash.State).amount}") println("txn1: ${txn1.id} spent ${((txn1.tx.outputs[0].data) as Cash.State).amount}")
@ -187,7 +168,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
database.transaction { database.transaction {
try { try {
val txn2Builder = TransactionBuilder(DUMMY_NOTARY) val txn2Builder = TransactionBuilder(DUMMY_NOTARY)
vault.generateSpend(txn2Builder, 80.DOLLARS, BOB) Cash.generateSpend(services, txn2Builder, 80.DOLLARS, BOB)
val ptxn2 = notaryServices.signInitialTransaction(txn2Builder) val ptxn2 = notaryServices.signInitialTransaction(txn2Builder)
val txn2 = services.addSignature(ptxn2, freshKey) val txn2 = services.addSignature(ptxn2, freshKey)
println("txn2: ${txn2.id} spent ${((txn2.tx.outputs[0].data) as Cash.State).amount}") println("txn2: ${txn2.id} spent ${((txn2.tx.outputs[0].data) as Cash.State).amount}")
@ -299,7 +280,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
database.transaction { database.transaction {
// A tx that spends our money. // A tx that spends our money.
val spendTXBuilder = TransactionBuilder(DUMMY_NOTARY) val spendTXBuilder = TransactionBuilder(DUMMY_NOTARY)
vault.generateSpend(spendTXBuilder, 80.DOLLARS, BOB) Cash.generateSpend(services, spendTXBuilder, 80.DOLLARS, BOB)
val spendPTX = notaryServices.signInitialTransaction(spendTXBuilder) val spendPTX = notaryServices.signInitialTransaction(spendTXBuilder)
val spendTX = services.addSignature(spendPTX, freshKey) val spendTX = services.addSignature(spendPTX, freshKey)
services.recordTransactions(spendTX) services.recordTransactions(spendTX)

View File

@ -34,7 +34,7 @@ class DummyIssueAndMove(private val notary: Party, private val counterpartyNode:
// Move ownership of the asset to the counterparty // Move ownership of the asset to the counterparty
val moveTxBuilder = TransactionBuilder(notary = notary) val moveTxBuilder = TransactionBuilder(notary = notary)
val (_, keys) = vaultService.generateSpend(moveTxBuilder, Amount(amount.quantity, GBP), counterpartyNode) val (_, keys) = Cash.generateSpend(serviceHub, moveTxBuilder, Amount(amount.quantity, GBP), counterpartyNode)
// We don't check signatures because we know that the notary's signature is missing // We don't check signatures because we know that the notary's signature is missing
signInitialTransaction(moveTxBuilder, keys) signInitialTransaction(moveTxBuilder, keys)
} }

View File

@ -13,9 +13,9 @@ import net.corda.core.identity.Party
import net.corda.core.node.ServiceHub import net.corda.core.node.ServiceHub
import net.corda.core.node.services.Vault import net.corda.core.node.services.Vault
import net.corda.core.toFuture import net.corda.core.toFuture
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.OpaqueBytes
import net.corda.testing.CHARLIE import net.corda.testing.CHARLIE
import net.corda.testing.DUMMY_NOTARY import net.corda.testing.DUMMY_NOTARY
import net.corda.testing.DUMMY_NOTARY_KEY import net.corda.testing.DUMMY_NOTARY_KEY
@ -228,10 +228,11 @@ fun ServiceHub.evolveLinearState(linearState: StateAndRef<LinearState>) : StateA
@JvmOverloads @JvmOverloads
fun ServiceHub.consumeCash(amount: Amount<Currency>, to: Party = CHARLIE): Vault.Update<ContractState> { fun ServiceHub.consumeCash(amount: Amount<Currency>, to: Party = CHARLIE): Vault.Update<ContractState> {
val update = vaultService.rawUpdates.toFuture() val update = vaultService.rawUpdates.toFuture()
val services = this
// A tx that spends our money. // A tx that spends our money.
val spendTX = TransactionBuilder(DUMMY_NOTARY).apply { val spendTX = TransactionBuilder(DUMMY_NOTARY).apply {
vaultService.generateSpend(this, amount, to) Cash.generateSpend(services, this, amount, to)
signWith(DUMMY_NOTARY_KEY) signWith(DUMMY_NOTARY_KEY)
}.toSignedTransaction(checkSufficientSignatures = false) }.toSignedTransaction(checkSufficientSignatures = false)

View File

@ -9,6 +9,7 @@ import net.corda.core.messaging.DataFeed
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.node.ServiceHub import net.corda.core.node.ServiceHub
import net.corda.core.node.services.* import net.corda.core.node.services.*
import net.corda.core.schemas.MappedSchema
import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SerializeAsToken
import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
@ -24,11 +25,14 @@ import net.corda.node.services.persistence.InMemoryStateMachineRecordedTransacti
import net.corda.node.services.schema.HibernateObserver import net.corda.node.services.schema.HibernateObserver
import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.schema.NodeSchemaService
import net.corda.node.services.transactions.InMemoryTransactionVerifierService import net.corda.node.services.transactions.InMemoryTransactionVerifierService
import net.corda.node.services.vault.HibernateVaultQueryImpl
import net.corda.node.services.vault.NodeVaultService import net.corda.node.services.vault.NodeVaultService
import net.corda.testing.DUMMY_CA import net.corda.node.utilities.CordaPersistence
import net.corda.testing.MEGA_CORP import net.corda.node.utilities.configureDatabase
import net.corda.testing.MOCK_IDENTITIES import net.corda.schemas.CashSchemaV1
import net.corda.testing.getTestPartyAndCertificate import net.corda.schemas.CommercialPaperSchemaV1
import net.corda.testing.*
import net.corda.testing.schemas.DummyLinearStateSchemaV1
import org.bouncycastle.operator.ContentSigner import org.bouncycastle.operator.ContentSigner
import rx.Observable import rx.Observable
import rx.subjects.PublishSubject import rx.subjects.PublishSubject
@ -212,4 +216,29 @@ fun makeTestDatabaseProperties(): Properties {
return props return props
} }
fun makeTestDatabaseAndMockServices(customSchemas: Set<MappedSchema> = setOf(CommercialPaperSchemaV1, DummyLinearStateSchemaV1, CashSchemaV1), keys: List<KeyPair> = listOf(MEGA_CORP_KEY)): Pair<CordaPersistence, MockServices> {
val dataSourceProps = makeTestDataSourceProperties()
val databaseProperties = makeTestDatabaseProperties()
val database = configureDatabase(dataSourceProps, databaseProperties)
val mockService = database.transaction {
val hibernateConfig = HibernateConfiguration(NodeSchemaService(customSchemas), databaseProperties)
object : MockServices(*(keys.toTypedArray())) {
override val vaultService: VaultService = makeVaultService(dataSourceProps, hibernateConfig)
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
for (stx in txs) {
validatedTransactions.addTransaction(stx)
}
// Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions.
vaultService.notifyAll(txs.map { it.tx })
}
override val vaultQueryService: VaultQueryService = HibernateVaultQueryImpl(hibernateConfig, vaultService.updatesPublisher)
override fun jdbcSession(): Connection = database.createSession()
}
}
return Pair(database, mockService)
}
val MOCK_VERSION_INFO = VersionInfo(1, "Mock release", "Mock revision", "Mock Vendor") val MOCK_VERSION_INFO = VersionInfo(1, "Mock release", "Mock revision", "Mock Vendor")