diff --git a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt index 4381d17de3..bcce50090a 100644 --- a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt +++ b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt @@ -15,6 +15,9 @@ import net.corda.core.node.NodeInfo import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.StateMachineTransactionMapping import net.corda.core.node.services.Vault +import net.corda.core.node.services.vault.PageSpecification +import net.corda.core.node.services.vault.QueryCriteria +import net.corda.core.node.services.vault.Sort import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction import org.bouncycastle.asn1.x500.X500Name @@ -65,10 +68,64 @@ interface CordaRPCOps : RPCOps { @RPCReturnsObservables fun stateMachinesAndUpdates(): Pair, Observable> + /** + * Returns a snapshot of vault states for a given query criteria (and optional order and paging specification) + * + * Generic vault query function which takes a [QueryCriteria] object to define filters, + * optional [PageSpecification] and optional [Sort] modification criteria (default unsorted), + * and returns a [Vault.Page] object containing the following: + * 1. states as a List of (page number and size defined by [PageSpecification]) + * 2. states metadata as a List of [Vault.StateMetadata] held in the Vault States table. + * 3. the [PageSpecification] used in the query + * 4. a total number of results available (for subsequent paging if necessary) + * + * Note: a default [PageSpecification] is applied to the query returning the 1st page (indexed from 0) with up to 200 entries. + * It is the responsibility of the Client to request further pages and/or specify a more suitable [PageSpecification]. + */ + // DOCSTART VaultQueryByAPI + fun vaultQueryBy(criteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(), + paging: PageSpecification = PageSpecification(), + sorting: Sort = Sort(emptySet())): Vault.Page + // DOCEND VaultQueryByAPI + + /** + * Returns a snapshot (as per queryBy) and an observable of future updates to the vault for the given query criteria. + * + * Generic vault query function which takes a [QueryCriteria] object to define filters, + * optional [PageSpecification] and optional [Sort] modification criteria (default unsorted), + * and returns a [Vault.PageAndUpdates] object containing + * 1) a snapshot as a [Vault.Page] (described previously in [queryBy]) + * 2) an [Observable] of [Vault.Update] + * + * Notes: the snapshot part of the query adheres to the same behaviour as the [queryBy] function. + * the [QueryCriteria] applies to both snapshot and deltas (streaming updates). + */ + // DOCSTART VaultTrackByAPI + @RPCReturnsObservables + fun vaultTrackBy(criteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(), + paging: PageSpecification = PageSpecification(), + sorting: Sort = Sort(emptySet())): Vault.PageAndUpdates + // DOCEND VaultTrackByAPI + + // Note: cannot apply @JvmOverloads to interfaces nor interface implementations + // Java Helpers + + // DOCSTART VaultQueryAPIJavaHelpers + fun vaultQueryByCriteria(criteria: QueryCriteria): Vault.Page = vaultQueryBy(criteria = criteria) + fun vaultQueryByWithPagingSpec(criteria: QueryCriteria, paging: PageSpecification): Vault.Page = vaultQueryBy(criteria, paging = paging) + fun vaultQueryByWithSorting(criteria: QueryCriteria, sorting: Sort): Vault.Page = vaultQueryBy(criteria, sorting = sorting) + + fun vaultTrackByCriteria(criteria: QueryCriteria): Vault.PageAndUpdates = vaultTrackBy(criteria = criteria) + fun vaultTrackByWithPagingSpec(criteria: QueryCriteria, paging: PageSpecification): Vault.PageAndUpdates = vaultTrackBy(criteria, paging = paging) + fun vaultTrackByWithSorting(criteria: QueryCriteria, sorting: Sort): Vault.PageAndUpdates = vaultTrackBy(criteria, sorting = sorting) + // DOCEND VaultQueryAPIJavaHelpers + /** * Returns a pair of head states in the vault and an observable of future updates to the vault. */ @RPCReturnsObservables + // TODO: Remove this from the interface + // @Deprecated("This function will be removed in a future milestone", ReplaceWith("vaultTrackBy(QueryCriteria())")) fun vaultAndUpdates(): Pair>, Observable> /** diff --git a/core/src/main/kotlin/net/corda/core/node/services/Services.kt b/core/src/main/kotlin/net/corda/core/node/services/Services.kt index a7598a4aa9..14c6e98c16 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/Services.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/Services.kt @@ -5,6 +5,9 @@ import com.google.common.util.concurrent.ListenableFuture import net.corda.core.contracts.* import net.corda.core.crypto.* import net.corda.core.flows.FlowException +import net.corda.core.node.services.vault.PageSpecification +import net.corda.core.node.services.vault.QueryCriteria +import net.corda.core.node.services.vault.Sort import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.OpaqueBytes import net.corda.core.toFuture @@ -16,6 +19,7 @@ import java.io.InputStream import java.security.KeyPair import java.security.PrivateKey import java.security.PublicKey +import java.time.Instant import java.util.* /** @@ -88,8 +92,38 @@ class Vault(val states: Iterable>) { } enum class StateStatus { - UNCONSUMED, CONSUMED + UNCONSUMED, CONSUMED, ALL } + + /** + * Returned in queries [VaultService.queryBy] and [VaultService.trackBy]. + * A Page contains: + * 1) a [List] of actual [StateAndRef] requested by the specified [QueryCriteria] to a maximum of [MAX_PAGE_SIZE] + * 2) a [List] of associated [Vault.StateMetadata], one per [StateAndRef] result + * 3) the [PageSpecification] definition used to bound this result set + * 4) a total number of states that met the given [QueryCriteria] + * Note that this may be more than the specified [PageSpecification.pageSize], and should be used to perform + * further pagination (by issuing new queries). + */ + @CordaSerializable + data class Page(val states: List>, + val statesMetadata: List, + val pageable: PageSpecification, + val totalStatesAvailable: Long) + + @CordaSerializable + data class StateMetadata(val ref: StateRef, + val contractStateClassName: String, + val recordedTime: Instant, + val consumedTime: Instant?, + val status: Vault.StateStatus, + val notaryName: String, + val notaryKey: String, + val lockId: String?, + val lockUpdateTime: Instant?) + + @CordaSerializable + data class PageAndUpdates (val current: Vault.Page, val future: Observable? = null) } /** @@ -129,12 +163,58 @@ interface VaultService { * Atomically get the current vault and a stream of updates. Note that the Observable buffers updates until the * first subscriber is registered so as to avoid racing with early updates. */ + // TODO: Remove this from the interface + // @Deprecated("This function will be removed in a future milestone", ReplaceWith("trackBy(QueryCriteria())")) fun track(): Pair, Observable> + // DOCSTART VaultQueryAPI + /** + * Generic vault query function which takes a [QueryCriteria] object to define filters, + * optional [PageSpecification] and optional [Sort] modification criteria (default unsorted), + * and returns a [Vault.Page] object containing the following: + * 1. states as a List of (page number and size defined by [PageSpecification]) + * 2. states metadata as a List of [Vault.StateMetadata] held in the Vault States table. + * 3. the [PageSpecification] used in the query + * 4. a total number of results available (for subsequent paging if necessary) + * + * Note: a default [PageSpecification] is applied to the query returning the 1st page (indexed from 0) with up to 200 entries. + * It is the responsibility of the Client to request further pages and/or specify a more suitable [PageSpecification]. + */ + fun queryBy(criteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(), + paging: PageSpecification = PageSpecification(), + sorting: Sort = Sort(emptySet())): Vault.Page + /** + * Generic vault query function which takes a [QueryCriteria] object to define filters, + * optional [PageSpecification] and optional [Sort] modification criteria (default unsorted), + * and returns a [Vault.PageAndUpdates] object containing + * 1) a snapshot as a [Vault.Page] (described previously in [queryBy]) + * 2) an [Observable] of [Vault.Update] + * + * Notes: the snapshot part of the query adheres to the same behaviour as the [queryBy] function. + * the [QueryCriteria] applies to both snapshot and deltas (streaming updates). + */ + fun trackBy(criteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(), + paging: PageSpecification = PageSpecification(), + sorting: Sort = Sort(emptySet())): Vault.PageAndUpdates + // DOCEND VaultQueryAPI + + // Note: cannot apply @JvmOverloads to interfaces nor interface implementations + // Java Helpers + fun queryBy(): Vault.Page = queryBy() + fun queryBy(criteria: QueryCriteria): Vault.Page = queryBy(criteria) + fun queryBy(criteria: QueryCriteria, paging: PageSpecification): Vault.Page = queryBy(criteria, paging) + fun queryBy(criteria: QueryCriteria, sorting: Sort): Vault.Page = queryBy(criteria, sorting) + + fun trackBy(): Vault.PageAndUpdates = trackBy() + fun trackBy(criteria: QueryCriteria): Vault.PageAndUpdates = trackBy(criteria) + fun trackBy(criteria: QueryCriteria, paging: PageSpecification): Vault.PageAndUpdates = trackBy(criteria, paging) + fun trackBy(criteria: QueryCriteria, sorting: Sort): Vault.PageAndUpdates = trackBy(criteria, Sort(emptySet())) + /** * Return unconsumed [ContractState]s for a given set of [StateRef]s - * TODO: revisit and generalize this exposed API function. */ + // TODO: Remove this from the interface + // @Deprecated("This function will be removed in a future milestone", ReplaceWith("queryBy(VaultQueryCriteria(stateRefs = listOf()))")) fun statesForRefs(refs: List): Map?> /** @@ -212,6 +292,8 @@ interface VaultService { * Return [ContractState]s of a given [Contract] type and [Iterable] of [Vault.StateStatus]. * Optionally may specify whether to include [StateRef] that have been marked as soft locked (default is true) */ + // TODO: Remove this from the interface + // @Deprecated("This function will be removed in a future milestone", ReplaceWith("queryBy(QueryCriteria())")) fun states(clazzes: Set>, statuses: EnumSet, includeSoftLockedStates: Boolean = true): Iterable> // DOCEND VaultStatesQuery @@ -257,17 +339,25 @@ interface VaultService { fun unconsumedStatesForSpending(amount: Amount, onlyFromIssuerParties: Set? = null, notary: Party? = null, lockId: UUID, withIssuerRefs: Set? = null): List> } +// TODO: Remove this from the interface +// @Deprecated("This function will be removed in a future milestone", ReplaceWith("queryBy(VaultQueryCriteria())")) inline fun VaultService.unconsumedStates(includeSoftLockedStates: Boolean = true): Iterable> = states(setOf(T::class.java), EnumSet.of(Vault.StateStatus.UNCONSUMED), includeSoftLockedStates) +// TODO: Remove this from the interface +// @Deprecated("This function will be removed in a future milestone", ReplaceWith("queryBy(VaultQueryCriteria(status = Vault.StateStatus.CONSUMED))")) inline fun VaultService.consumedStates(): Iterable> = states(setOf(T::class.java), EnumSet.of(Vault.StateStatus.CONSUMED)) /** Returns the [linearState] heads only when the type of the state would be considered an 'instanceof' the given type. */ +// TODO: Remove this from the interface +// @Deprecated("This function will be removed in a future milestone", ReplaceWith("queryBy(LinearStateQueryCriteria(linearId = listOf()))")) inline fun VaultService.linearHeadsOfType() = states(setOf(T::class.java), EnumSet.of(Vault.StateStatus.UNCONSUMED)) .associateBy { it.state.data.linearId }.mapValues { it.value } +// TODO: Remove this from the interface +// @Deprecated("This function will be removed in a future milestone", ReplaceWith("queryBy(LinearStateQueryCriteria(dealPartyName = listOf()))")) inline fun VaultService.dealsWith(party: AbstractParty) = linearHeadsOfType().values.filter { it.state.data.parties.any { it == party } } diff --git a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt new file mode 100644 index 0000000000..efd19f8df8 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt @@ -0,0 +1,85 @@ +package net.corda.core.node.services.vault + +import net.corda.core.contracts.Commodity +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.UniqueIdentifier +import net.corda.core.node.services.Vault +import net.corda.core.node.services.vault.QueryCriteria.AndComposition +import net.corda.core.node.services.vault.QueryCriteria.OrComposition +import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.OpaqueBytes +import java.time.Instant +import java.util.* + +/** + * Indexing assumptions: + * QueryCriteria assumes underlying schema tables are correctly indexed for performance. + */ +@CordaSerializable +sealed class QueryCriteria { + + /** + * VaultQueryCriteria: provides query by attributes defined in [VaultSchema.VaultStates] + */ + data class VaultQueryCriteria @JvmOverloads constructor ( + val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, + val stateRefs: List? = null, + val contractStateTypes: Set>? = null, + val notaryName: List? = null, + val includeSoftlockedStates: Boolean? = true, + val timeCondition: Logical>? = null, + val participantIdentities: List? = null) : QueryCriteria() + + /** + * LinearStateQueryCriteria: provides query by attributes defined in [VaultSchema.VaultLinearState] + */ + data class LinearStateQueryCriteria @JvmOverloads constructor( + val linearId: List? = null, + val latestOnly: Boolean? = true, + val dealRef: List? = null, + val dealPartyName: List? = null) : QueryCriteria() + + /** + * FungibleStateQueryCriteria: provides query by attributes defined in [VaultSchema.VaultFungibleState] + * + * Valid TokenType implementations defined by Amount are + * [Currency] as used in [Cash] contract state + * [Commodity] as used in [CommodityContract] state + */ + data class FungibleAssetQueryCriteria @JvmOverloads constructor( + val ownerIdentity: List? = null, + val quantity: Logical<*,Long>? = null, + val tokenType: List>? = null, + val tokenValue: List? = null, + val issuerPartyName: List? = null, + val issuerRef: List? = null, + val exitKeyIdentity: List? = null) : QueryCriteria() + + /** + * VaultCustomQueryCriteria: provides query by custom attributes defined in a contracts + * [QueryableState] implementation. + * (see Persistence documentation for more information) + * + * Params + * [indexExpression] refers to a (composable) JPA Query like WHERE expression clauses of the form: + * [JPA entityAttributeName] [Operand] [Value] + * + * Refer to [CommercialPaper.State] for a concrete example. + */ + data class VaultCustomQueryCriteria(val indexExpression: Logical? = null) : QueryCriteria() + + // enable composition of [QueryCriteria] + data class AndComposition(val a: QueryCriteria, val b: QueryCriteria): QueryCriteria() + data class OrComposition(val a: QueryCriteria, val b: QueryCriteria): QueryCriteria() + + // timestamps stored in the vault states table [VaultSchema.VaultStates] + @CordaSerializable + enum class TimeInstantType { + RECORDED, + CONSUMED + } +} + +infix fun QueryCriteria.and(criteria: QueryCriteria): QueryCriteria = AndComposition(this, criteria) +infix fun QueryCriteria.or(criteria: QueryCriteria): QueryCriteria = OrComposition(this, criteria) diff --git a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt new file mode 100644 index 0000000000..fc53fc2d02 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt @@ -0,0 +1,113 @@ +package net.corda.core.node.services.vault + +import net.corda.core.serialization.CordaSerializable + +@CordaSerializable +enum class Operator { + AND, + OR, + EQUAL, + NOT_EQUAL, + LESS_THAN, + LESS_THAN_OR_EQUAL, + GREATER_THAN, + GREATER_THAN_OR_EQUAL, + IN, + NOT_IN, + LIKE, + NOT_LIKE, + BETWEEN, + IS_NULL, + NOT_NULL +} + +interface Condition { + val leftOperand: L + val operator: Operator + val rightOperand: R +} + +interface AndOr { + infix fun and(condition: Condition): Q + infix fun or(condition: Condition): Q +} + +@CordaSerializable +sealed class Logical : Condition, AndOr> + +class LogicalExpression(leftOperand: L, + operator: Operator, + rightOperand: R? = null) : Logical() { + init { + if (rightOperand == null) { + check(operator in setOf(Operator.NOT_NULL, Operator.IS_NULL), + { "Must use a unary operator (${Operator.IS_NULL}, ${Operator.NOT_NULL}) if right operand is null"} ) + } + else { + check(operator !in setOf(Operator.NOT_NULL, Operator.IS_NULL), + { "Cannot use a unary operator (${Operator.IS_NULL}, ${Operator.NOT_NULL}) if right operand is not null"} ) + } + } + override fun and(condition: Condition): Logical<*, *> = LogicalExpression(this, Operator.AND, condition) + override fun or(condition: Condition): Logical<*, *> = LogicalExpression(this, Operator.OR, condition) + + override val operator: Operator = operator + override val rightOperand: R = rightOperand as R + override val leftOperand: L = leftOperand +} + + +/** + * Pagination and Ordering + * + * Provide simple ability to specify an offset within a result set and the number of results to + * return from that offset (eg. page size) together with (optional) sorting criteria at column level. + * + * Note: it is the responsibility of the calling client to manage page windows. + * + * For advanced pagination it is recommended you utilise standard JPA query frameworks such as + * Spring Data's JPARepository which extends the [PagingAndSortingRepository] interface to provide + * paging and sorting capability: + * https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/repository/PagingAndSortingRepository.html + */ +val DEFAULT_PAGE_NUM = 0L +val DEFAULT_PAGE_SIZE = 200L + +/** + * Note: this maximum size will be configurable in future (to allow for large JVM heap sized node configurations) + * Use [PageSpecification] to correctly handle a number of bounded pages of [MAX_PAGE_SIZE]. + */ +val MAX_PAGE_SIZE = 512L + +/** + * PageSpecification allows specification of a page number (starting from 0 as default) and page size (defaulting to + * [DEFAULT_PAGE_SIZE] with a maximum page size of [DEFAULT_PAGE_SIZE] + */ +@CordaSerializable +data class PageSpecification(val pageNumber: Long = DEFAULT_PAGE_NUM, val pageSize: Long = DEFAULT_PAGE_SIZE) + +/** + * Sort allows specification of a set of entity attribute names and their associated directionality + * and null handling, to be applied upon processing a query specification. + */ +@CordaSerializable +data class Sort(val columns: Collection) { + @CordaSerializable + enum class Direction { + ASC, + DESC + } + @CordaSerializable + enum class NullHandling { + NULLS_FIRST, + NULLS_LAST + } + /** + * [columnName] should reference a persisted entity attribute name as defined by the associated mapped schema + * (for example, [VaultSchema.VaultStates::txId.name]) + */ + @CordaSerializable + data class SortColumn(val columnName: String, val direction: Sort.Direction = Sort.Direction.ASC, + val nullHandling: Sort.NullHandling = if (direction == Sort.Direction.ASC) Sort.NullHandling.NULLS_LAST else Sort.NullHandling.NULLS_FIRST) +} + diff --git a/docs/source/key-concepts-vault.rst b/docs/source/key-concepts-vault.rst index 4ccbf7b773..33ef7f6bdc 100644 --- a/docs/source/key-concepts-vault.rst +++ b/docs/source/key-concepts-vault.rst @@ -46,7 +46,7 @@ Note the following: * the vault performs fungible state spending (and in future, fungible state optimisation management including merging, splitting and re-issuance) * vault extensions represent additional custom plugin code a developer may write to query specific custom contract state attributes. * customer "Off Ledger" (private store) represents internal organisational data that may be joined with the vault data to perform additional reporting or processing -* a vault query API is exposed to developers using standard Corda RPC and CorDapp plugin mechanisms +* a :doc:`vault-query` API is exposed to developers using standard Corda RPC and CorDapp plugin mechanisms * a vault update API is internally used by transaction recording flows. * the vault database schemas are directly accessible via JDBC for customer joins and queries diff --git a/docs/source/vault-query.rst b/docs/source/vault-query.rst new file mode 100644 index 0000000000..2cbe5c31e6 --- /dev/null +++ b/docs/source/vault-query.rst @@ -0,0 +1,339 @@ +Vault Query +=========== + +Corda has been architected from the ground up to encourage usage of industry standard, proven query frameworks and libraries for accessing RDBMS backed transactional stores (including the Vault). + +Corda provides a number of flexible query mechanisms for accessing the Vault: + +- Vault Query API +- custom JPA_/JPQL_ queries +- custom 3rd party Data Access frameworks such as `Spring Data `_ + +The majority of query requirements can be satified by using the Vault Query API, which is exposed via the ``VaultService`` for use directly by flows: + +.. literalinclude:: ../../core/src/main/kotlin/net/corda/core/node/services/Services.kt + :language: kotlin + :start-after: DOCSTART VaultQueryAPI + :end-before: DOCEND VaultQueryAPI + +and via ``CordaRPCOps`` for use by RPC client applications: + +.. literalinclude:: ../../core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt + :language: kotlin + :start-after: DOCSTART VaultQueryByAPI + :end-before: DOCEND VaultQueryByAPI + +.. literalinclude:: ../../core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt + :language: kotlin + :start-after: DOCSTART VaultTrackByAPI + :end-before: DOCEND VaultTrackByAPI + +Java helper methods are also provided with default values for arguments: + +.. literalinclude:: ../../core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt + :language: kotlin + :start-after: DOCSTART VaultQueryAPIJavaHelpers + :end-before: DOCEND VaultQueryAPIJavaHelpers + +The API provides both static (snapshot) and dynamic (snapshot with streaming updates) methods for a defined set of filter criteria. + +- Use ``queryBy`` to obtain a only current snapshot of data (for a given ``QueryCriteria``) +- Use ``trackBy`` to obtain a both a current snapshot and a future stream of updates (for a given ``QueryCriteria``) + +Simple pagination (page number and size) and sorting (directional ordering, null handling, custom property sort) is also specifiable. +Defaults are defined for Paging (pageNumber = 0, pageSize = 200) and Sorting (direction = ASC, nullHandling = NULLS_LAST). + +The ``QueryCriteria`` interface provides a flexible mechanism for specifying different filtering criteria, including and/or composition and a rich set of logical operators. There are four implementations of this interface which can be chained together to define advanced filters. + + 1. ``VaultQueryCriteria`` provides filterable criteria on attributes within the Vault states table: status (UNCONSUMED, CONSUMED), state reference(s), contract state type(s), notaries, soft locked states, timestamps (RECORDED, CONSUMED). + + .. note:: Sensible defaults are defined for frequently used attributes (status = UNCONSUMED, includeSoftlockedStates = true). + + 2. ``FungibleAssetQueryCriteria`` provides filterable criteria on attributes defined in the Corda Core ``FungibleAsset`` contract state interface, used to represent assets that are fungible, countable and issued by a specific party (eg. ``Cash.State`` and ``CommodityContract.State`` in the Corda finance module). + + .. note:: Contract states that extend the ``FungibleAsset`` interface now automatically persist associated state attributes. + + 3. ``LinearStateQueryCriteria`` provides filterable criteria on attributes defined in the Corda Core ``LinearState`` and ``DealState`` contract state interfaces, used to represent entities that continuously supercede themselves, all of which share the same *linearId* (eg. trade entity states such as the ``IRSState`` defined in the SIMM valuation demo) + + .. note:: Contract states that extend the ``LinearState`` or ``DealState`` interfaces now automatically persist associated state attributes. + + 4. ``VaultCustomQueryCriteria`` provides the means to specify one or many arbitrary expressions on attributes defined by a custom contract state that implements its own schema as described in the Persistence_ documentation and associated examples. + Custom criteria expressions are expressed as JPA Query like WHERE clauses as follows: [JPA entityAttributeName] [Operand] [Value] + + An example is illustrated here: + +.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt + :language: kotlin + :start-after: DOCSTART VaultQueryExample16 + :end-before: DOCEND VaultQueryExample16 + +All ``QueryCriteria`` implementations are composable using ``and`` and ``or`` operators, as also illustrated above. + +Additional notes + + .. note:: Custom contract states that implement the ``Queryable`` interface may now extend the ``FungiblePersistentState``, ``LinearPersistentState`` or ``DealPersistentState`` classes when implementing their ``MappedSchema``. Previously, all custom contracts extended the root ``PersistentState`` class and defined repeated mappings of ``FungibleAsset``, ``LinearState`` and ``DealState`` attributes. + +Examples of these ``QueryCriteria`` objects are presented below for Kotlin and Java. + +The Vault Query API leverages the rich semantics of the underlying Requery_ persistence framework adopted by Corda. + +.. _Requery: https://github.com/requery/requery/wiki +.. _Persistence: https://docs.corda.net/persistence.html + +.. note:: Permissioning at the database level will be enforced at a later date to ensure authenticated, role-based, read-only access to underlying Corda tables. + +.. note:: API's now provide ease of use calling semantics from both Java and Kotlin. + +.. note:: Current queries by ``Party`` specify only a party name as the underlying identity framework is being re-designed. In future it may be possible to query on specific elements of a parties identity such as a ``CompositeKey`` hierarchy (parent and child nodes, weightings). + +Example usage +------------- + +Kotlin +^^^^^^ + +**General snapshot queries using** ``VaultQueryCriteria`` + +Query for all unconsumed states: + +.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt + :language: kotlin + :start-after: DOCSTART VaultQueryExample1 + :end-before: DOCEND VaultQueryExample1 + +Query for unconsumed states for some state references: + +.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt + :language: kotlin + :start-after: DOCSTART VaultQueryExample2 + :end-before: DOCEND VaultQueryExample2 + +Query for unconsumed states for several contract state types: + +.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt + :language: kotlin + :start-after: DOCSTART VaultQueryExample3 + :end-before: DOCEND VaultQueryExample3 + +Query for unconsumed states for a given notary: + +.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt + :language: kotlin + :start-after: DOCSTART VaultQueryExample4 + :end-before: DOCEND VaultQueryExample4 + +Query for unconsumed states for a given set of participants: + +.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt + :language: kotlin + :start-after: DOCSTART VaultQueryExample5 + :end-before: DOCEND VaultQueryExample5 + +Query for unconsumed states recorded between two time intervals: + +.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt + :language: kotlin + :start-after: DOCSTART VaultQueryExample6 + :end-before: DOCEND VaultQueryExample6 + +Query for all states with pagination specification: + +.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt + :language: kotlin + :start-after: DOCSTART VaultQueryExample7 + :end-before: DOCEND VaultQueryExample7 + +**LinearState and DealState queries using** ``LinearStateQueryCriteria`` + +Query for unconsumed linear states for given linear ids: + +.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt + :language: kotlin + :start-after: DOCSTART VaultQueryExample8 + :end-before: DOCEND VaultQueryExample8 + +.. note:: This example was previously executed using the deprecated extension method: + + .. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt + :language: kotlin + :start-after: DOCSTART VaultDeprecatedQueryExample1 + :end-before: DOCEND VaultDeprecatedQueryExample1 + +Query for all linear states associated with a linear id: + +.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt + :language: kotlin + :start-after: DOCSTART VaultQueryExample9 + :end-before: DOCEND VaultQueryExample9 + +.. note:: This example was previously executed using the deprecated method: + + .. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt + :language: kotlin + :start-after: DOCSTART VaultDeprecatedQueryExample2 + :end-before: DOCEND VaultDeprecatedQueryExample2 + +Query for unconsumed deal states with deals references: + +.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt + :language: kotlin + :start-after: DOCSTART VaultQueryExample10 + :end-before: DOCEND VaultQueryExample10 + +Query for unconsumed deal states with deals parties: + +.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt + :language: kotlin + :start-after: DOCSTART VaultQueryExample11 + :end-before: DOCEND VaultQueryExample11 + +**FungibleAsset and DealState queries using** ``FungibleAssetQueryCriteria`` + +Query for fungible assets for a given currency: + +.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt + :language: kotlin + :start-after: DOCSTART VaultQueryExample12 + :end-before: DOCEND VaultQueryExample12 + +Query for fungible assets for a given currency and minimum quantity: + +.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt + :language: kotlin + :start-after: DOCSTART VaultQueryExample13 + :end-before: DOCEND VaultQueryExample13 + +Query for fungible assets for a specifc issuer party: + +.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt + :language: kotlin + :start-after: DOCSTART VaultQueryExample14 + :end-before: DOCEND VaultQueryExample14 + +Query for consumed fungible assets with a specific exit key: + +.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt + :language: kotlin + :start-after: DOCSTART VaultQueryExample15 + :end-before: DOCEND VaultQueryExample15 + +Java examples +^^^^^^^^^^^^^ + +Query for all consumed contract states: + +.. literalinclude:: ../../node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java + :language: java + :start-after: DOCSTART VaultJavaQueryExample1 + :end-before: DOCEND VaultJavaQueryExample1 + +.. note:: This example was previously executed using the deprecated method: + + .. literalinclude:: ../../node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java + :language: java + :start-after: DOCSTART VaultDeprecatedJavaQueryExample1 + :end-before: DOCEND VaultDeprecatedJavaQueryExample1 + +Query for all deal states: + +.. literalinclude:: ../../node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java + :language: java + :start-after: DOCSTART VaultJavaQueryExample2 + :end-before: DOCEND VaultJavaQueryExample2 + +.. note:: This example was previously executed using the deprecated method: + + .. literalinclude:: ../../node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java + :language: java + :start-after: DOCSTART VaultDeprecatedJavaQueryExample2 + :end-before: DOCEND VaultDeprecatedJavaQueryExample2 + +**Dynamic queries** (also using ``VaultQueryCriteria``) are an extension to the snapshot queries by returning an additional ``QueryResults`` return type in the form of an ``Observable``. Refer to `ReactiveX Observable `_ for a detailed understanding and usage of this type. + +Other use case scenarios +------------------------ + +For advanced use cases that require sophisticated pagination, sorting, grouping, and aggregation functions, it is recommended that the CorDapp developer utilise one of the many proven frameworks that ship with this capability out of the box. Namely, implementations of JPQL (JPA Query Language) such as **Hibernate** for advanced SQL access, and **Spring Data** for advanced pagination and ordering constructs. + +The Corda Tutorials provide examples satisfying these additional Use Cases: + + 1. Template / Tutorial CorDapp service using Vault API Custom Query to access attributes of IOU State + 2. Template / Tutorial CorDapp service query extension executing Named Queries via JPQL_ + 3. `Advanced pagination `_ queries using Spring Data JPA_ + + .. _JPQL: http://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#hql + .. _JPA: https://docs.spring.io/spring-data/jpa/docs/current/reference/html + +Upgrading from previous releases +--------------------------------- + +Here follows a selection of the most common upgrade scenarios: + +1. ServiceHub usage to obtain Unconsumed states for a given contract state type + + Previously: + +.. container:: codeset + + .. sourcecode:: kotlin + + val yoStates = b.vault.unconsumedStates() + +This query returned an ``Iterable>`` + + Now: + +.. container:: codeset + + .. sourcecode:: kotlin + + val yoStates = b.vault.queryBy().states + +The query returns a ``Vault.Page`` result containing: + + - states as a ``List>`` sized according to the default Page specification of ``DEFAULT_PAGE_NUM`` (0) and ``DEFAULT_PAGE_SIZE`` (200). + - states metadata as a ``List`` containing Vault State metadata held in the Vault states table. + - the ``PagingSpecification`` used in the query + - a ``total`` number of results available. This value can be used issue subsequent queries with appropriately specified ``PageSpecification`` (according to your paging needs and/or maximum memory capacity for holding large data sets). Note it is your responsibility to manage page numbers and sizes. + +2. ServiceHub usage obtaining linear heads for a given contract state type + + Previously: + +.. container:: codeset + + .. sourcecode:: kotlin + + val iouStates = serviceHub.vaultService.linearHeadsOfType() + val iouToSettle = iouStates[linearId] ?: throw Exception("IOUState with linearId $linearId not found.") + + Now: + +.. container:: codeset + + .. sourcecode:: kotlin + + val criteria = QueryCriteria.LinearStateQueryCriteria(linearId = listOf(linearId)) + val iouStates = serviceHub.vaultService.queryBy(criteria).states + + val iouToSettle = iouStates.singleOrNull() ?: throw Exception("IOUState with linearId $linearId not found.") + +3. RPC usage was limited to using the ``vaultAndUpdates`` RPC method, which returned a snapshot and streaming updates as an Observable. + In many cases, queries were not interested in the streaming updates. + + Previously: + +.. container:: codeset + + .. sourcecode:: kotlin + + val iouStates = services.vaultAndUpdates().first.filter { it.state.data is IOUState } + + Now: + +.. container:: codeset + + .. sourcecode:: kotlin + + val iouStates = services.vaultQueryBy() + diff --git a/finance/build.gradle b/finance/build.gradle index f88c937b36..e7cf52b57d 100644 --- a/finance/build.gradle +++ b/finance/build.gradle @@ -10,6 +10,7 @@ description 'Corda finance modules' dependencies { compile project(':core') + compile project(':node-schemas') testCompile project(':test-utils') testCompile project(path: ':core', configuration: 'testArtifacts') diff --git a/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt b/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt index d68f0f36a1..e40c3a1e9a 100644 --- a/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt +++ b/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt @@ -81,13 +81,14 @@ class CommercialPaper : Contract { override fun withFaceValue(newFaceValue: Amount>): ICommercialPaperState = copy(faceValue = newFaceValue) override fun withMaturityDate(newMaturityDate: Instant): ICommercialPaperState = copy(maturityDate = newMaturityDate) + // DOCSTART VaultIndexedQueryCriteria /** Object Relational Mapping support. */ override fun supportedSchemas(): Iterable = listOf(CommercialPaperSchemaV1) /** Object Relational Mapping support. */ override fun generateMappedObject(schema: MappedSchema): PersistentState { return when (schema) { - is CommercialPaperSchemaV1 -> CommercialPaperSchemaV1.PersistentCommericalPaperState( + is CommercialPaperSchemaV1 -> CommercialPaperSchemaV1.PersistentCommercialPaperState( issuanceParty = this.issuance.party.owningKey.toBase58String(), issuanceRef = this.issuance.reference.bytes, owner = this.owner.toBase58String(), @@ -100,6 +101,7 @@ class CommercialPaper : Contract { else -> throw IllegalArgumentException("Unrecognised schema $schema") } } + // DOCEND VaultIndexedQueryCriteria } interface Clauses { diff --git a/finance/src/main/kotlin/net/corda/contracts/testing/VaultFiller.kt b/finance/src/main/kotlin/net/corda/contracts/testing/VaultFiller.kt index 9a9e1a326c..4cb790ba15 100644 --- a/finance/src/main/kotlin/net/corda/contracts/testing/VaultFiller.kt +++ b/finance/src/main/kotlin/net/corda/contracts/testing/VaultFiller.kt @@ -5,10 +5,8 @@ package net.corda.contracts.testing import net.corda.contracts.asset.Cash import net.corda.contracts.asset.DUMMY_CASH_ISSUER import net.corda.contracts.asset.DUMMY_CASH_ISSUER_KEY -import net.corda.core.contracts.Amount -import net.corda.core.contracts.Issued -import net.corda.core.contracts.PartyAndReference -import net.corda.core.contracts.TransactionType +import net.corda.core.contracts.* +import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.Party import net.corda.core.node.ServiceHub import net.corda.core.node.services.Vault @@ -20,12 +18,16 @@ import java.security.KeyPair import java.security.PublicKey import java.util.* -fun ServiceHub.fillWithSomeTestDeals(dealIds: List) { +@JvmOverloads +fun ServiceHub.fillWithSomeTestDeals(dealIds: List, + revisions: Int? = 0, + participants: List = emptyList()) : Vault { val freshKey = keyManagementService.freshKey() + val transactions: List = dealIds.map { // Issue a deal state val dummyIssue = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { - addOutputState(DummyDealContract.State(ref = it, participants = listOf(freshKey.public))) + addOutputState(DummyDealContract.State(ref = it, participants = participants.plus(freshKey.public))) signWith(freshKey) signWith(DUMMY_NOTARY_KEY) } @@ -33,19 +35,40 @@ fun ServiceHub.fillWithSomeTestDeals(dealIds: List) { } recordTransactions(transactions) + + // Get all the StateAndRefs of all the generated transactions. + val states = transactions.flatMap { stx -> + stx.tx.outputs.indices.map { i -> stx.tx.outRef(i) } + } + + return Vault(states) } -fun ServiceHub.fillWithSomeTestLinearStates(numberToCreate: Int) { +@JvmOverloads +fun ServiceHub.fillWithSomeTestLinearStates(numberToCreate: Int, + uid: UniqueIdentifier = UniqueIdentifier(), + participants: List = emptyList()) : Vault { val freshKey = keyManagementService.freshKey() - for (i in 1..numberToCreate) { - // Issue a deal state + + val transactions: List = (1..numberToCreate).map { + // Issue a Linear state val dummyIssue = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { - addOutputState(DummyLinearContract.State(participants = listOf(freshKey.public))) + addOutputState(DummyLinearContract.State(linearId = uid, participants = participants.plus(freshKey.public))) signWith(freshKey) signWith(DUMMY_NOTARY_KEY) } - recordTransactions(dummyIssue.toSignedTransaction()) + + return@map dummyIssue.toSignedTransaction(true) } + + recordTransactions(transactions) + + // Get all the StateAndRefs of all the generated transactions. + val states = transactions.flatMap { stx -> + stx.tx.outputs.indices.map { i -> stx.tx.outRef(i) } + } + + return Vault(states) } /** diff --git a/finance/src/main/kotlin/net/corda/schemas/CashSchemaV1.kt b/finance/src/main/kotlin/net/corda/schemas/CashSchemaV1.kt index 8972dd9254..27b4b3aed6 100644 --- a/finance/src/main/kotlin/net/corda/schemas/CashSchemaV1.kt +++ b/finance/src/main/kotlin/net/corda/schemas/CashSchemaV1.kt @@ -4,6 +4,7 @@ import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState import javax.persistence.Column import javax.persistence.Entity +import javax.persistence.Index import javax.persistence.Table /** @@ -17,7 +18,9 @@ object CashSchema */ object CashSchemaV1 : MappedSchema(schemaFamily = CashSchema.javaClass, version = 1, mappedTypes = listOf(PersistentCashState::class.java)) { @Entity - @Table(name = "cash_states") + @Table(name = "cash_states", + indexes = arrayOf(Index(name = "ccy_code_idx", columnList = "ccy_code"), + Index(name = "pennies_idx", columnList = "pennies"))) class PersistentCashState( @Column(name = "owner_key") var owner: String, diff --git a/finance/src/main/kotlin/net/corda/schemas/CommercialPaperSchemaV1.kt b/finance/src/main/kotlin/net/corda/schemas/CommercialPaperSchemaV1.kt index 4181855b70..3a1fcb8a21 100644 --- a/finance/src/main/kotlin/net/corda/schemas/CommercialPaperSchemaV1.kt +++ b/finance/src/main/kotlin/net/corda/schemas/CommercialPaperSchemaV1.kt @@ -5,6 +5,7 @@ import net.corda.core.schemas.PersistentState import java.time.Instant import javax.persistence.Column import javax.persistence.Entity +import javax.persistence.Index import javax.persistence.Table /** @@ -16,10 +17,13 @@ object CommercialPaperSchema * First version of a commercial paper contract ORM schema that maps all fields of the [CommercialPaper] contract state * as it stood at the time of writing. */ -object CommercialPaperSchemaV1 : MappedSchema(schemaFamily = CommercialPaperSchema.javaClass, version = 1, mappedTypes = listOf(PersistentCommericalPaperState::class.java)) { +object CommercialPaperSchemaV1 : MappedSchema(schemaFamily = CommercialPaperSchema.javaClass, version = 1, mappedTypes = listOf(PersistentCommercialPaperState::class.java)) { @Entity - @Table(name = "cp_states") - class PersistentCommericalPaperState( + @Table(name = "cp_states", + indexes = arrayOf(Index(name = "ccy_code_idx", columnList = "ccy_code"), + Index(name = "maturity_idx", columnList = "maturity_instant"), + Index(name = "face_value_idx", columnList = "face_value"))) + class PersistentCommercialPaperState( @Column(name = "issuance_key") var issuanceParty: String, diff --git a/node-schemas/build.gradle b/node-schemas/build.gradle index 3b176b06d3..70faf99703 100644 --- a/node-schemas/build.gradle +++ b/node-schemas/build.gradle @@ -9,6 +9,7 @@ dependencies { compile project(':core') testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" testCompile "junit:junit:$junit_version" + testCompile project(':test-utils') // Requery: SQL based query & persistence for Kotlin kapt "io.requery:requery-processor:$requery_version" diff --git a/node-schemas/generated/source/kapt/main/net/corda/node/services/vault/schemas/Models.java b/node-schemas/generated/source/kapt/main/net/corda/node/services/vault/schemas/Models.java new file mode 100644 index 0000000000..e69de29bb2 diff --git a/node-schemas/src/main/kotlin/net/corda/node/services/vault/schemas/VaultSchema.kt b/node-schemas/src/main/kotlin/net/corda/node/services/vault/schemas/VaultSchema.kt index 6100f27a33..dd3a43743b 100644 --- a/node-schemas/src/main/kotlin/net/corda/node/services/vault/schemas/VaultSchema.kt +++ b/node-schemas/src/main/kotlin/net/corda/node/services/vault/schemas/VaultSchema.kt @@ -4,6 +4,7 @@ import io.requery.* import net.corda.core.node.services.Vault import net.corda.core.schemas.requery.Requery import java.time.Instant +import java.util.* object VaultSchema { @@ -73,4 +74,20 @@ object VaultSchema { @get:Column(name = "lock_timestamp", nullable = true) var lockUpdateTime: Instant? } + + /** + * The following entity is for illustration purposes only as used by VaultQueryTests + */ + @Table(name = "vault_linear_states") + @Entity(model = "vault") + interface VaultLinearState : Persistable { + + @get:Index("external_id_index") + @get:Column(name = "external_id") + var externalId: String + + @get:Index("uuid_index") + @get:Column(name = "uuid", unique = true, nullable = false) + var uuid: UUID + } } \ No newline at end of file diff --git a/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt b/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt index 7a40588f09..4b768992ae 100644 --- a/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt +++ b/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt @@ -29,7 +29,6 @@ import org.junit.Before import org.junit.Test import rx.Observable import java.security.PublicKey -import sun.misc.MessageUtils.where import java.time.Instant import java.util.* import java.util.concurrent.CountDownLatch diff --git a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt index d26b67bc81..c07e41ef51 100644 --- a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt @@ -15,6 +15,9 @@ import net.corda.core.node.NodeInfo import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.StateMachineTransactionMapping import net.corda.core.node.services.Vault +import net.corda.core.node.services.vault.PageSpecification +import net.corda.core.node.services.vault.QueryCriteria +import net.corda.core.node.services.vault.Sort import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction import net.corda.node.services.api.ServiceHubInternal @@ -55,6 +58,23 @@ class CordaRPCOpsImpl( } } + override fun vaultQueryBy(criteria: QueryCriteria, + paging: PageSpecification, + sorting: Sort): Vault.Page { + return database.transaction { + services.vaultService.queryBy(criteria, paging, sorting) + } + } + + @RPCReturnsObservables + override fun vaultTrackBy(criteria: QueryCriteria, + paging: PageSpecification, + sorting: Sort): Vault.PageAndUpdates { + return database.transaction { + services.vaultService.trackBy(criteria, paging, sorting) + } + } + override fun verifiedTransactions(): Pair, Observable> { return database.transaction { services.storageService.validatedTransactions.track() diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index ddfb1f843e..59dfdd7607 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -15,18 +15,15 @@ import net.corda.contracts.clause.AbstractConserveAmount import net.corda.core.ThreadBox import net.corda.core.bufferUntilSubscribed import net.corda.core.contracts.* -import net.corda.core.crypto.AbstractParty -import net.corda.core.crypto.CompositeKey -import net.corda.core.crypto.Party -import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.containsAny -import net.corda.core.crypto.toBase58String -import net.corda.core.flows.FlowStateMachine +import net.corda.core.crypto.* import net.corda.core.node.ServiceHub import net.corda.core.node.services.StatesNotAvailableException import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultService import net.corda.core.node.services.unconsumedStates +import net.corda.core.node.services.vault.PageSpecification +import net.corda.core.node.services.vault.QueryCriteria +import net.corda.core.node.services.vault.Sort import net.corda.core.serialization.* import net.corda.core.tee import net.corda.core.transactions.TransactionBuilder @@ -194,6 +191,7 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P .map { it -> val stateRef = StateRef(SecureHash.parse(it.txId), it.index) val state = it.contractState.deserialize>(storageKryo()) + Vault.StateMetadata(stateRef, it.contractStateClassName, it.recordedTime, it.consumedTime, it.stateStatus, it.notaryName, it.notaryKey, it.lockId, it.lockUpdateTime) StateAndRef(state, stateRef) } } @@ -221,6 +219,26 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P return stateAndRefs.associateBy({ it.ref }, { it.state }) } + override fun queryBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): Vault.Page { + + TODO("Under construction") + + // If [VaultQueryCriteria.PageSpecification] specified + // must return (CloseableIterator) result.get().iterator(skip, take) + // where + // skip = Max[(pageNumber - 1),0] * pageSize + // take = pageSize + } + + override fun trackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): Vault.PageAndUpdates { + TODO("Under construction") + +// return mutex.locked { +// Vault.PageAndUpdates(queryBy(criteria), +// _updatesPublisher.bufferUntilSubscribed().wrapWithDatabaseTransaction()) +// } + } + override fun notifyAll(txns: Iterable) { val ourKeys = services.keyManagementService.keys.keys val netDelta = txns.fold(Vault.NoUpdate) { netDelta, txn -> netDelta + makeUpdate(txn, ourKeys) } @@ -520,4 +538,4 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P private fun stateRefArgs(stateRefs: Iterable): List> { return stateRefs.map { listOf("'${it.txhash}'", it.index) } } -} \ No newline at end of file +} diff --git a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java new file mode 100644 index 0000000000..a2b9f8c254 --- /dev/null +++ b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java @@ -0,0 +1,280 @@ +package net.corda.node.services.vault; + +import com.google.common.collect.*; +import kotlin.*; +import net.corda.contracts.asset.*; +import net.corda.core.contracts.*; +import net.corda.core.crypto.*; +import net.corda.core.node.services.*; +import net.corda.core.node.services.vault.*; +import net.corda.core.node.services.vault.QueryCriteria.*; +import net.corda.core.serialization.*; +import net.corda.core.transactions.*; +import net.corda.node.services.vault.schemas.*; +import net.corda.testing.node.*; +import org.jetbrains.annotations.*; +import org.jetbrains.exposed.sql.*; +import org.junit.*; +import rx.Observable; + +import java.io.*; +import java.util.*; +import java.util.stream.*; + +import static net.corda.contracts.asset.CashKt.*; +import static net.corda.contracts.testing.VaultFiller.*; +import static net.corda.core.node.services.vault.QueryCriteriaKt.*; +import static net.corda.core.node.services.vault.QueryCriteriaUtilsKt.*; +import static net.corda.core.utilities.TestConstants.*; +import static net.corda.node.utilities.DatabaseSupportKt.*; +import static net.corda.node.utilities.DatabaseSupportKt.transaction; +import static net.corda.testing.CoreTestUtils.*; +import static net.corda.testing.node.MockServicesKt.*; +import static org.assertj.core.api.Assertions.*; + +@Ignore +public class VaultQueryJavaTests { + + private MockServices services; + private VaultService vaultSvc; + private Closeable dataSource; + private Database database; + + @Before + public void setUp() { + + Properties dataSourceProps = makeTestDataSourceProperties(SecureHash.randomSHA256().toString()); + Pair dataSourceAndDatabase = configureDatabase(dataSourceProps); + dataSource = dataSourceAndDatabase.getFirst(); + database = dataSourceAndDatabase.getSecond(); + + transaction(database, statement -> services = new MockServices() { + @NotNull + @Override + public VaultService getVaultService() { + return makeVaultService(dataSourceProps); + } + + @Override + public void recordTransactions(@NotNull Iterable txs) { + for (SignedTransaction stx : txs ) { + getStorageService().getValidatedTransactions().addTransaction(stx); + } + + Stream wtxn = StreamSupport.stream(txs.spliterator(), false).map(txn -> txn.getTx()); + getVaultService().notifyAll(wtxn.collect(Collectors.toList())); + } + }); + + vaultSvc = services.getVaultService(); + } + + @After + public void cleanUp() throws IOException { + dataSource.close(); + } + + /** + * Sample Vault Query API tests + */ + + /** + * Static queryBy() tests + */ + + @Test + public void consumedStates() { + transaction(database, tx -> { + fillWithSomeTestCash(services, + new Amount<>(100, Currency.getInstance("USD")), + getDUMMY_NOTARY(), + 3, + 3, + new Random(), + new OpaqueBytes("1".getBytes()), + null, + getDUMMY_CASH_ISSUER(), + getDUMMY_CASH_ISSUER_KEY() ); + + // DOCSTART VaultJavaQueryExample1 + @SuppressWarnings("unchecked") + Set> contractStateTypes = new HashSet(Collections.singletonList(Cash.State.class)); + Vault.StateStatus status = Vault.StateStatus.CONSUMED; + + VaultQueryCriteria criteria = new VaultQueryCriteria(status, null, contractStateTypes); + Vault.Page results = vaultSvc.queryBy(criteria); + // DOCEND VaultJavaQueryExample1 + + assertThat(results.getStates()).hasSize(3); + + return tx; + }); + } + + @Test + public void consumedDealStatesPagedSorted() { + transaction(database, tx -> { + + UniqueIdentifier uid = new UniqueIdentifier(); + fillWithSomeTestLinearStates(services, 10, uid); + + List dealIds = Arrays.asList("123", "456", "789"); + fillWithSomeTestDeals(services, dealIds, 0); + + // DOCSTART VaultJavaQueryExample2 + Vault.StateStatus status = Vault.StateStatus.CONSUMED; + @SuppressWarnings("unchecked") + Set> contractStateTypes = new HashSet(Collections.singletonList(Cash.State.class)); + + QueryCriteria vaultCriteria = new VaultQueryCriteria(status, null, contractStateTypes); + + List linearIds = Arrays.asList(uid); + List dealPartyNames = Arrays.asList(getMEGA_CORP().getName()); + QueryCriteria dealCriteriaAll = new LinearStateQueryCriteria(linearIds, false, dealIds, dealPartyNames); + + QueryCriteria compositeCriteria = and(dealCriteriaAll, vaultCriteria); + + PageSpecification pageSpec = new PageSpecification(0, getMAX_PAGE_SIZE()); + Sort.SortColumn sortByUid = new Sort.SortColumn(VaultLinearStateEntity.UUID.getName(), Sort.Direction.DESC, Sort.NullHandling.NULLS_LAST); + Sort sorting = new Sort(ImmutableSet.of(sortByUid)); + Vault.Page results = vaultSvc.queryBy(compositeCriteria, pageSpec, sorting); + // DOCEND VaultJavaQueryExample2 + + assertThat(results.getStates()).hasSize(4); + + return tx; + }); + } + + /** + * Dynamic trackBy() tests + */ + + @Test + public void trackCashStates() { + + transaction(database, tx -> { + fillWithSomeTestCash(services, + new Amount<>(100, Currency.getInstance("USD")), + getDUMMY_NOTARY(), + 3, + 3, + new Random(), + new OpaqueBytes("1".getBytes()), + null, + getDUMMY_CASH_ISSUER(), + getDUMMY_CASH_ISSUER_KEY() ); + + // DOCSTART VaultJavaQueryExample1 + @SuppressWarnings("unchecked") + Set> contractStateTypes = new HashSet(Collections.singletonList(Cash.State.class)); + + VaultQueryCriteria criteria = new VaultQueryCriteria(Vault.StateStatus.UNCONSUMED, null, contractStateTypes); + Vault.PageAndUpdates results = vaultSvc.trackBy(criteria); + + Vault.Page snapshot = results.getCurrent(); + Observable updates = results.getFuture(); + + // DOCEND VaultJavaQueryExample1 + assertThat(snapshot.getStates()).hasSize(3); + + return tx; + }); + } + + @Test + public void trackDealStatesPagedSorted() { + transaction(database, tx -> { + + UniqueIdentifier uid = new UniqueIdentifier(); + fillWithSomeTestLinearStates(services, 10, uid); + + List dealIds = Arrays.asList("123", "456", "789"); + fillWithSomeTestDeals(services, dealIds, 0); + + // DOCSTART VaultJavaQueryExample2 + @SuppressWarnings("unchecked") + Set> contractStateTypes = new HashSet(Collections.singletonList(DealState.class)); + QueryCriteria vaultCriteria = new VaultQueryCriteria(Vault.StateStatus.UNCONSUMED, null, contractStateTypes); + + List linearIds = Arrays.asList(uid); + List dealPartyNames = Arrays.asList(getMEGA_CORP().getName()); + QueryCriteria dealCriteriaAll = new LinearStateQueryCriteria(linearIds, false, dealIds, dealPartyNames); + + QueryCriteria compositeCriteria = and(dealCriteriaAll, vaultCriteria); + + PageSpecification pageSpec = new PageSpecification(0, getMAX_PAGE_SIZE()); + Sort.SortColumn sortByUid = new Sort.SortColumn(VaultLinearStateEntity.UUID.getName(), Sort.Direction.DESC, Sort.NullHandling.NULLS_LAST); + Sort sorting = new Sort(ImmutableSet.of(sortByUid)); + Vault.PageAndUpdates results = vaultSvc.trackBy(compositeCriteria, pageSpec, sorting); + + Vault.Page snapshot = results.getCurrent(); + Observable updates = results.getFuture(); + // DOCEND VaultJavaQueryExample2 + + assertThat(snapshot.getStates()).hasSize(4); + + return tx; + }); + } + + /** + * Deprecated usage + */ + + @Test + public void consumedStatesDeprecated() { + transaction(database, tx -> { + fillWithSomeTestCash(services, + new Amount<>(100, Currency.getInstance("USD")), + getDUMMY_NOTARY(), + 3, + 3, + new Random(), + new OpaqueBytes("1".getBytes()), + null, + getDUMMY_CASH_ISSUER(), + getDUMMY_CASH_ISSUER_KEY() ); + + // DOCSTART VaultDeprecatedJavaQueryExample1 + @SuppressWarnings("unchecked") + Set> contractStateTypes = new HashSet(Collections.singletonList(Cash.State.class)); + EnumSet status = EnumSet.of(Vault.StateStatus.CONSUMED); + + // WARNING! unfortunately cannot use inlined reified Kotlin extension methods. + Iterable> results = vaultSvc.states(contractStateTypes, status, true); + // DOCEND VaultDeprecatedJavaQueryExample1 + + assertThat(results).hasSize(3); + + return tx; + }); + } + + @Test + public void consumedStatesForLinearIdDeprecated() { + transaction(database, tx -> { + + UniqueIdentifier trackUid = new UniqueIdentifier(); + fillWithSomeTestLinearStates(services, 1, trackUid); + fillWithSomeTestLinearStates(services, 4, new UniqueIdentifier()); + + // DOCSTART VaultDeprecatedJavaQueryExample2 + @SuppressWarnings("unchecked") + Set> contractStateTypes = new HashSet(Collections.singletonList(LinearState.class)); + EnumSet status = EnumSet.of(Vault.StateStatus.CONSUMED); + + // WARNING! unfortunately cannot use inlined reified Kotlin extension methods. + Iterable> results = vaultSvc.states(contractStateTypes, status, true); + + Stream> trackedLinearState = StreamSupport.stream(results.spliterator(), false).filter( + state -> ((LinearState) state.component1().getData()).getLinearId() == trackUid); + // DOCEND VaultDeprecatedJavaQueryExample2 + + assertThat(results).hasSize(4); + assertThat(trackedLinearState).hasSize(1); + + return tx; + }); + } +} diff --git a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt index a90f367498..8190cd02cf 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt @@ -35,7 +35,7 @@ import kotlin.test.assertNull class NodeVaultServiceTest { lateinit var services: MockServices - val vault: VaultService get() = services.vaultService + val vaultSvc: VaultService get() = services.vaultService lateinit var dataSource: Closeable lateinit var database: Database @@ -73,11 +73,11 @@ class NodeVaultServiceTest { services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) - val w1 = services.vaultService.unconsumedStates() + val w1 = vaultSvc.unconsumedStates() assertThat(w1).hasSize(3) val originalStorage = services.storageService - val originalVault = services.vaultService + val originalVault = vaultSvc val services2 = object : MockServices() { override val vaultService: VaultService get() = originalVault @@ -103,11 +103,11 @@ class NodeVaultServiceTest { services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) - val w1 = services.vaultService.unconsumedStates().toList() + val w1 = vaultSvc.unconsumedStates().toList() assertThat(w1).hasSize(3) val stateRefs = listOf(w1[1].ref, w1[2].ref) - val states = services.vaultService.statesForRefs(stateRefs) + val states = vaultSvc.statesForRefs(stateRefs) assertThat(states).hasSize(2) } } @@ -118,32 +118,32 @@ class NodeVaultServiceTest { services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) - val unconsumedStates = services.vaultService.unconsumedStates().toList() + val unconsumedStates = vaultSvc.unconsumedStates().toList() assertThat(unconsumedStates).hasSize(3) val stateRefsToSoftLock = setOf(unconsumedStates[1].ref, unconsumedStates[2].ref) // soft lock two of the three states val softLockId = UUID.randomUUID() - services.vaultService.softLockReserve(softLockId, stateRefsToSoftLock) + vaultSvc.softLockReserve(softLockId, stateRefsToSoftLock) // all softlocked states - assertThat(services.vaultService.softLockedStates()).hasSize(2) + assertThat(vaultSvc.softLockedStates()).hasSize(2) // my softlocked states - assertThat(services.vaultService.softLockedStates(softLockId)).hasSize(2) + assertThat(vaultSvc.softLockedStates(softLockId)).hasSize(2) // excluding softlocked states - val unlockedStates1 = services.vaultService.unconsumedStates(includeSoftLockedStates = false).toList() + val unlockedStates1 = vaultSvc.unconsumedStates(includeSoftLockedStates = false).toList() assertThat(unlockedStates1).hasSize(1) // soft lock release one of the states explicitly - services.vaultService.softLockRelease(softLockId, setOf(unconsumedStates[1].ref)) - val unlockedStates2 = services.vaultService.unconsumedStates(includeSoftLockedStates = false).toList() + vaultSvc.softLockRelease(softLockId, setOf(unconsumedStates[1].ref)) + val unlockedStates2 = vaultSvc.unconsumedStates(includeSoftLockedStates = false).toList() assertThat(unlockedStates2).hasSize(2) // soft lock release the rest by id - services.vaultService.softLockRelease(softLockId) - val unlockedStates = services.vaultService.unconsumedStates(includeSoftLockedStates = false).toList() + vaultSvc.softLockRelease(softLockId) + val unlockedStates = vaultSvc.unconsumedStates(includeSoftLockedStates = false).toList() assertThat(unlockedStates).hasSize(3) // should be back to original states @@ -162,7 +162,7 @@ class NodeVaultServiceTest { val vaultStates = database.transaction { - assertNull(vault.cashBalances[USD]) + assertNull(vaultSvc.cashBalances[USD]) services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) } val stateRefsToSoftLock = vaultStates.states.map { it.ref }.toSet() @@ -172,8 +172,8 @@ class NodeVaultServiceTest { backgroundExecutor.submit { try { database.transaction { - vault.softLockReserve(softLockId1, stateRefsToSoftLock) - assertThat(vault.softLockedStates(softLockId1)).hasSize(3) + vaultSvc.softLockReserve(softLockId1, stateRefsToSoftLock) + assertThat(vaultSvc.softLockedStates(softLockId1)).hasSize(3) } println("SOFT LOCK STATES #1 succeeded") } catch(e: Throwable) { @@ -188,8 +188,8 @@ class NodeVaultServiceTest { try { Thread.sleep(100) // let 1st thread soft lock them 1st database.transaction { - vault.softLockReserve(softLockId2, stateRefsToSoftLock) - assertThat(vault.softLockedStates(softLockId2)).hasSize(3) + vaultSvc.softLockReserve(softLockId2, stateRefsToSoftLock) + assertThat(vaultSvc.softLockedStates(softLockId2)).hasSize(3) } println("SOFT LOCK STATES #2 succeeded") } catch(e: Throwable) { @@ -201,10 +201,10 @@ class NodeVaultServiceTest { countDown.await() database.transaction { - val lockStatesId1 = vault.softLockedStates(softLockId1) + val lockStatesId1 = vaultSvc.softLockedStates(softLockId1) println("SOFT LOCK #1 final states: $lockStatesId1") assertThat(lockStatesId1.size).isIn(0, 3) - val lockStatesId2 = vault.softLockedStates(softLockId2) + val lockStatesId2 = vaultSvc.softLockedStates(softLockId2) println("SOFT LOCK #2 final states: $lockStatesId2") assertThat(lockStatesId2.size).isIn(0, 3) } @@ -218,7 +218,7 @@ class NodeVaultServiceTest { val vaultStates = database.transaction { - assertNull(vault.cashBalances[USD]) + assertNull(vaultSvc.cashBalances[USD]) services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) } val stateRefsToSoftLock = vaultStates.states.map { it.ref }.toSet() @@ -226,14 +226,14 @@ class NodeVaultServiceTest { // lock 1st state with LockId1 database.transaction { - vault.softLockReserve(softLockId1, setOf(stateRefsToSoftLock.first())) - assertThat(vault.softLockedStates(softLockId1)).hasSize(1) + vaultSvc.softLockReserve(softLockId1, setOf(stateRefsToSoftLock.first())) + assertThat(vaultSvc.softLockedStates(softLockId1)).hasSize(1) } // attempt to lock all 3 states with LockId2 database.transaction { assertThatExceptionOfType(StatesNotAvailableException::class.java).isThrownBy( - { vault.softLockReserve(softLockId2, stateRefsToSoftLock) } + { vaultSvc.softLockReserve(softLockId2, stateRefsToSoftLock) } ).withMessageContaining("only 2 rows available").withNoCause() } } @@ -245,7 +245,7 @@ class NodeVaultServiceTest { val vaultStates = database.transaction { - assertNull(vault.cashBalances[USD]) + assertNull(vaultSvc.cashBalances[USD]) services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) } val stateRefsToSoftLock = vaultStates.states.map { it.ref }.toSet() @@ -253,14 +253,14 @@ class NodeVaultServiceTest { // lock states with LockId1 database.transaction { - vault.softLockReserve(softLockId1, stateRefsToSoftLock) - assertThat(vault.softLockedStates(softLockId1)).hasSize(3) + vaultSvc.softLockReserve(softLockId1, stateRefsToSoftLock) + assertThat(vaultSvc.softLockedStates(softLockId1)).hasSize(3) } // attempt to relock same states with LockId1 database.transaction { - vault.softLockReserve(softLockId1, stateRefsToSoftLock) - assertThat(vault.softLockedStates(softLockId1)).hasSize(3) + vaultSvc.softLockReserve(softLockId1, stateRefsToSoftLock) + assertThat(vaultSvc.softLockedStates(softLockId1)).hasSize(3) } } @@ -271,7 +271,7 @@ class NodeVaultServiceTest { val vaultStates = database.transaction { - assertNull(vault.cashBalances[USD]) + assertNull(vaultSvc.cashBalances[USD]) services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) } val stateRefsToSoftLock = vaultStates.states.map { it.ref }.toSet() @@ -279,14 +279,14 @@ class NodeVaultServiceTest { // lock states with LockId1 database.transaction { - vault.softLockReserve(softLockId1, setOf(stateRefsToSoftLock.first())) - assertThat(vault.softLockedStates(softLockId1)).hasSize(1) + vaultSvc.softLockReserve(softLockId1, setOf(stateRefsToSoftLock.first())) + assertThat(vaultSvc.softLockedStates(softLockId1)).hasSize(1) } // attempt to lock all states with LockId1 (including previously already locked one) database.transaction { - vault.softLockReserve(softLockId1, stateRefsToSoftLock) - assertThat(vault.softLockedStates(softLockId1)).hasSize(3) + vaultSvc.softLockReserve(softLockId1, stateRefsToSoftLock) + assertThat(vaultSvc.softLockedStates(softLockId1)).hasSize(3) } } @@ -296,14 +296,14 @@ class NodeVaultServiceTest { services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) - val unconsumedStates = services.vaultService.unconsumedStates().toList() + val unconsumedStates = vaultSvc.unconsumedStates().toList() assertThat(unconsumedStates).hasSize(1) - val spendableStatesUSD = (services.vaultService as NodeVaultService).unconsumedStatesForSpending(100.DOLLARS, lockId = UUID.randomUUID()) + val spendableStatesUSD = (vaultSvc as NodeVaultService).unconsumedStatesForSpending(100.DOLLARS, lockId = UUID.randomUUID()) spendableStatesUSD.forEach(::println) assertThat(spendableStatesUSD).hasSize(1) assertThat(spendableStatesUSD[0].state.data.amount.quantity).isEqualTo(100L * 100) - assertThat(services.vaultService.softLockedStates()).hasSize(1) + assertThat(vaultSvc.softLockedStates()).hasSize(1) } } @@ -314,7 +314,7 @@ class NodeVaultServiceTest { services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (DUMMY_CASH_ISSUER)) services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(1)), issuerKey = BOC_KEY) - val spendableStatesUSD = services.vaultService.unconsumedStatesForSpending(200.DOLLARS, lockId = UUID.randomUUID(), + val spendableStatesUSD = vaultSvc.unconsumedStatesForSpending(200.DOLLARS, lockId = UUID.randomUUID(), onlyFromIssuerParties = setOf(DUMMY_CASH_ISSUER.party, BOC)).toList() spendableStatesUSD.forEach(::println) assertThat(spendableStatesUSD).hasSize(2) @@ -332,10 +332,10 @@ class NodeVaultServiceTest { services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(2)), issuerKey = BOC_KEY, ref = OpaqueBytes.of(2)) services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(3)), issuerKey = BOC_KEY, ref = OpaqueBytes.of(3)) - val unconsumedStates = services.vaultService.unconsumedStates().toList() + val unconsumedStates = vaultSvc.unconsumedStates().toList() assertThat(unconsumedStates).hasSize(4) - val spendableStatesUSD = services.vaultService.unconsumedStatesForSpending(200.DOLLARS, lockId = UUID.randomUUID(), + val spendableStatesUSD = vaultSvc.unconsumedStatesForSpending(200.DOLLARS, lockId = UUID.randomUUID(), onlyFromIssuerParties = setOf(BOC), withIssuerRefs = setOf(OpaqueBytes.of(1), OpaqueBytes.of(2))).toList() assertThat(spendableStatesUSD).hasSize(2) assertThat(spendableStatesUSD[0].state.data.amount.token.issuer.party).isEqualTo(BOC) @@ -350,13 +350,13 @@ class NodeVaultServiceTest { services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) - val unconsumedStates = services.vaultService.unconsumedStates().toList() + val unconsumedStates = vaultSvc.unconsumedStates().toList() assertThat(unconsumedStates).hasSize(1) - val spendableStatesUSD = (services.vaultService as NodeVaultService).unconsumedStatesForSpending(110.DOLLARS, lockId = UUID.randomUUID()) + val spendableStatesUSD = (vaultSvc as NodeVaultService).unconsumedStatesForSpending(110.DOLLARS, lockId = UUID.randomUUID()) spendableStatesUSD.forEach(::println) assertThat(spendableStatesUSD).hasSize(1) - assertThat(services.vaultService.softLockedStates()).hasSize(0) + assertThat(vaultSvc.softLockedStates()).hasSize(0) } } @@ -366,14 +366,14 @@ class NodeVaultServiceTest { services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 2, 2, Random(0L)) - val unconsumedStates = services.vaultService.unconsumedStates().toList() + val unconsumedStates = vaultSvc.unconsumedStates().toList() assertThat(unconsumedStates).hasSize(2) - val spendableStatesUSD = (services.vaultService as NodeVaultService).unconsumedStatesForSpending(1.DOLLARS, lockId = UUID.randomUUID()) + val spendableStatesUSD = (vaultSvc as NodeVaultService).unconsumedStatesForSpending(1.DOLLARS, lockId = UUID.randomUUID()) spendableStatesUSD.forEach(::println) assertThat(spendableStatesUSD).hasSize(1) assertThat(spendableStatesUSD[0].state.data.amount.quantity).isGreaterThanOrEqualTo(100L) - assertThat(services.vaultService.softLockedStates()).hasSize(1) + assertThat(vaultSvc.softLockedStates()).hasSize(1) } } @@ -385,15 +385,15 @@ class NodeVaultServiceTest { services.fillWithSomeTestCash(100.POUNDS, DUMMY_NOTARY, 10, 10, Random(0L)) services.fillWithSomeTestCash(100.SWISS_FRANCS, DUMMY_NOTARY, 10, 10, Random(0L)) - val allStates = services.vaultService.unconsumedStates() + val allStates = vaultSvc.unconsumedStates() assertThat(allStates).hasSize(30) for (i in 1..5) { - val spendableStatesUSD = (services.vaultService as NodeVaultService).unconsumedStatesForSpending(20.DOLLARS, lockId = UUID.randomUUID()) + val spendableStatesUSD = (vaultSvc as NodeVaultService).unconsumedStatesForSpending(20.DOLLARS, lockId = UUID.randomUUID()) spendableStatesUSD.forEach(::println) } // note only 3 spend attempts succeed with a total of 8 states - assertThat(services.vaultService.softLockedStates()).hasSize(8) + assertThat(vaultSvc.softLockedStates()).hasSize(8) } } @@ -411,10 +411,10 @@ class NodeVaultServiceTest { services.recordTransactions(listOf(usefulTX)) - services.vaultService.addNoteToTransaction(usefulTX.id, "USD Sample Note 1") - services.vaultService.addNoteToTransaction(usefulTX.id, "USD Sample Note 2") - services.vaultService.addNoteToTransaction(usefulTX.id, "USD Sample Note 3") - assertEquals(3, services.vaultService.getTransactionNotes(usefulTX.id).count()) + vaultSvc.addNoteToTransaction(usefulTX.id, "USD Sample Note 1") + vaultSvc.addNoteToTransaction(usefulTX.id, "USD Sample Note 2") + vaultSvc.addNoteToTransaction(usefulTX.id, "USD Sample Note 3") + assertEquals(3, vaultSvc.getTransactionNotes(usefulTX.id).count()) // Issue more Money (GBP) val anotherTX = TransactionType.General.Builder(null).apply { @@ -424,8 +424,8 @@ class NodeVaultServiceTest { services.recordTransactions(listOf(anotherTX)) - services.vaultService.addNoteToTransaction(anotherTX.id, "GPB Sample Note 1") - assertEquals(1, services.vaultService.getTransactionNotes(anotherTX.id).count()) + vaultSvc.addNoteToTransaction(anotherTX.id, "GPB Sample Note 1") + assertEquals(1, vaultSvc.getTransactionNotes(anotherTX.id).count()) } } } diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt new file mode 100644 index 0000000000..d40a5b0a6a --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -0,0 +1,782 @@ +package net.corda.node.services.vault + +import net.corda.contracts.CommercialPaper +import net.corda.contracts.asset.Cash +import net.corda.contracts.asset.DUMMY_CASH_ISSUER +import net.corda.contracts.testing.fillWithSomeTestCash +import net.corda.contracts.testing.fillWithSomeTestDeals +import net.corda.contracts.testing.fillWithSomeTestLinearStates +import net.corda.core.contracts.* +import net.corda.core.crypto.Party +import net.corda.core.crypto.entropyToKeyPair +import net.corda.core.days +import net.corda.core.node.services.Vault +import net.corda.core.node.services.VaultService +import net.corda.core.node.services.linearHeadsOfType +import net.corda.core.node.services.vault.* +import net.corda.core.node.services.vault.QueryCriteria.* +import net.corda.core.seconds +import net.corda.core.serialization.OpaqueBytes +import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.* +import net.corda.node.services.vault.schemas.VaultLinearStateEntity +import net.corda.node.services.vault.schemas.VaultSchema +import net.corda.node.utilities.configureDatabase +import net.corda.node.utilities.transaction +import net.corda.schemas.CashSchemaV1.PersistentCashState +import net.corda.schemas.CommercialPaperSchemaV1.PersistentCommercialPaperState +import net.corda.testing.* +import net.corda.testing.node.MockServices +import net.corda.testing.node.makeTestDataSourceProperties +import org.assertj.core.api.Assertions.assertThat +import org.jetbrains.exposed.sql.Database +import org.junit.After +import org.junit.Before +import org.junit.Ignore +import org.junit.Test +import java.io.Closeable +import java.math.BigInteger +import java.security.KeyPair +import java.time.temporal.ChronoUnit +import java.util.* + +@Ignore +class VaultQueryTests { + lateinit var services: MockServices + val vaultSvc: VaultService get() = services.vaultService + lateinit var dataSource: Closeable + lateinit var database: Database + + @Before + fun setUp() { + val dataSourceProps = makeTestDataSourceProperties() + val dataSourceAndDatabase = configureDatabase(dataSourceProps) + dataSource = dataSourceAndDatabase.first + database = dataSourceAndDatabase.second + database.transaction { + services = object : MockServices() { + override val vaultService: VaultService = makeVaultService(dataSourceProps) + + override fun recordTransactions(txs: Iterable) { + for (stx in txs) { + storageService.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 }) + } + } + } + } + + @After + fun tearDown() { + dataSource.close() + } + + /** + * Query API tests + */ + + /** Generic Query tests + (combining both FungibleState and LinearState contract types) */ + + @Test + fun `unconsumed states`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) + services.fillWithSomeTestLinearStates(10) + services.fillWithSomeTestDeals(listOf("123", "456", "789")) + + // DOCSTART VaultQueryExample1 + val criteria = VaultQueryCriteria() // default is UNCONSUMED + val result = vaultSvc.queryBy(criteria) + + /** + * Query result returns a [Vault.Page] which contains: + * 1) actual states as a list of [StateAndRef] + * 2) state reference and associated vault metadata as a list of [Vault.StateMetadata] + * 3) [PageSpecification] used to delimit the size of items returned in the result set (defaults to [DEFAULT_PAGE_SIZE]) + * 4) Total number of items available (to aid further pagination if required) + */ + val states = result.states + val metadata = result.statesMetadata + + // DOCEND VaultQueryExample1 + assertThat(states).hasSize(16) + assertThat(metadata).hasSize(16) + } + } + + @Test + fun `unconsumed states for state refs`() { + database.transaction { + + val issuedStates = services.fillWithSomeTestLinearStates(2) + val stateRefs = issuedStates.states.map { it.ref }.toList() + services.fillWithSomeTestLinearStates(8) + + // DOCSTART VaultQueryExample2 + val criteria = VaultQueryCriteria(stateRefs = listOf(stateRefs.first(), stateRefs.last())) + val results = vaultSvc.queryBy(criteria) + // DOCEND VaultQueryExample2 + + assertThat(results.states).hasSize(2) + assertThat(results.states.first().ref).isEqualTo(issuedStates.states.first().ref) + assertThat(results.states.last().ref).isEqualTo(issuedStates.states.last().ref) + } + } + + @Test + fun `unconsumed states for contract state types`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) + services.fillWithSomeTestLinearStates(10) + services.fillWithSomeTestDeals(listOf("123", "456", "789")) + + // default State.Status is UNCONSUMED + // DOCSTART VaultQueryExample3 + val criteria = VaultQueryCriteria(contractStateTypes = setOf(Cash.State::class.java, DealState::class.java)) + val results = vaultSvc.queryBy(criteria) + // DOCEND VaultQueryExample3 + assertThat(results.states).hasSize(6) + } + } + + @Test + fun `consumed states`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) + services.fillWithSomeTestLinearStates(2, UniqueIdentifier("TEST")) // create 2 states with same UID + services.fillWithSomeTestLinearStates(8) + services.fillWithSomeTestDeals(listOf("123", "456", "789")) + +// services.consumeLinearStates(UniqueIdentifier("TEST")) +// services.consumeDeals("456") +// services.consumeCash(80.DOLLARS) + + val criteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED) + val results = vaultSvc.queryBy(criteria) + assertThat(results.states).hasSize(5) + } + } + + @Test + fun `all states`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) + services.fillWithSomeTestLinearStates(2, UniqueIdentifier("TEST")) // create 2 results with same UID + services.fillWithSomeTestLinearStates(8) + services.fillWithSomeTestDeals(listOf("123", "456", "789")) + +// services.consumeLinearStates(UniqueIdentifier("TEST")) +// services.consumeDeals("456") +// services.consumeCash(80.DOLLARS) + + val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) + val results = vaultSvc.queryBy(criteria) + assertThat(results.states).hasSize(16) + } + } + + + val CASH_NOTARY_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(20)) } + val CASH_NOTARY: Party get() = Party("Notary Service", CASH_NOTARY_KEY.public) + + @Test + fun `unconsumed states by notary`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, CASH_NOTARY, 3, 3, Random(0L)) + services.fillWithSomeTestLinearStates(10) + services.fillWithSomeTestDeals(listOf("123", "456", "789")) + + // DOCSTART VaultQueryExample4 + val criteria = VaultQueryCriteria(notaryName = listOf(CASH_NOTARY.name)) + val results = vaultSvc.queryBy(criteria) + // DOCEND VaultQueryExample4 + assertThat(results.states).hasSize(3) + } + } + + @Test + fun `unconsumed states by participants`() { + database.transaction { + + services.fillWithSomeTestLinearStates(2, UniqueIdentifier("TEST"), participants = listOf(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY)) + services.fillWithSomeTestDeals(listOf("456"), 3, participants = listOf(MEGA_CORP_PUBKEY, BIG_CORP_PUBKEY)) + services.fillWithSomeTestDeals(listOf("123", "789"), participants = listOf(BIG_CORP_PUBKEY, MINI_CORP_PUBKEY)) + + // DOCSTART VaultQueryExample5 + val criteria = VaultQueryCriteria(participantIdentities = listOf(MEGA_CORP.name, MINI_CORP.name)) + val results = vaultSvc.queryBy(criteria) + // DOCEND VaultQueryExample5 + + assertThat(results.states).hasSize(3) + } + } + + @Test + fun `unconsumed states excluding soft locks`() { + database.transaction { + + val issuedStates = services.fillWithSomeTestCash(100.DOLLARS, CASH_NOTARY, 3, 3, Random(0L)) + vaultSvc.softLockReserve(UUID.randomUUID(), setOf(issuedStates.states.first().ref, issuedStates.states.last().ref)) + + val criteria = VaultQueryCriteria(includeSoftlockedStates = false) + val results = vaultSvc.queryBy(criteria) + assertThat(results.states).hasSize(1) + } + } + + @Test + fun `unconsumed states recorded between two time intervals`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, CASH_NOTARY, 3, 3, Random(0L)) + services.fillWithSomeTestLinearStates(10) + services.fillWithSomeTestDeals(listOf("123", "456", "789")) + + // DOCSTART VaultQueryExample6 + val start = TEST_TX_TIME + val end = TEST_TX_TIME.plus(30, ChronoUnit.DAYS) + val recordedBetweenExpression = LogicalExpression( + QueryCriteria.TimeInstantType.RECORDED, Operator.BETWEEN, arrayOf(start, end)) + val criteria = VaultQueryCriteria(timeCondition = recordedBetweenExpression) + val results = vaultSvc.queryBy(criteria) + // DOCEND VaultQueryExample6 + assertThat(results.states).hasSize(3) + } + } + + @Test + fun `states consumed after time`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, CASH_NOTARY, 3, 3, Random(0L)) + services.fillWithSomeTestLinearStates(10) + services.fillWithSomeTestDeals(listOf("123", "456", "789")) + + val asOfDateTime = TEST_TX_TIME + val consumedAfterExpression = LogicalExpression( + QueryCriteria.TimeInstantType.CONSUMED, Operator.GREATER_THAN, arrayOf(asOfDateTime)) + val criteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED, + timeCondition = consumedAfterExpression) + val results = vaultSvc.queryBy(criteria) + + assertThat(results.states).hasSize(3) + } + } + + // pagination: first page + @Test + fun `all states with paging specification - first page`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 100, 100, Random(0L)) + + // DOCSTART VaultQueryExample7 + val pagingSpec = PageSpecification(DEFAULT_PAGE_NUM, 10) + val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) + val results = vaultSvc.queryBy(criteria, paging = pagingSpec) + // DOCEND VaultQueryExample7 + assertThat(results.states).hasSize(10) + } + } + + // pagination: last page + @Test + fun `all states with paging specification - last`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 100, 100, Random(0L)) + + // Last page implies we need to perform a row count for the Query first, + // and then re-query for a given offset defined by (count - pageSize) + val pagingSpec = PageSpecification(-1, 10) + + val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) + val results = vaultSvc.queryBy(criteria, paging = pagingSpec) + assertThat(results.states).hasSize(10) // should retrieve states 90..99 + } + } + + @Test + fun `unconsumed fungible assets`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) +// services.fillWithSomeTestCommodity() + services.fillWithSomeTestLinearStates(10) + + val criteria = VaultQueryCriteria(contractStateTypes = setOf(FungibleAsset::class.java)) // default is UNCONSUMED + val results = vaultSvc.queryBy>(criteria) + assertThat(results.states).hasSize(4) + } + } + + @Test + fun `consumed fungible assets`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) +// services.consumeCash(2) +// services.fillWithSomeTestCommodity() +// services.consumeCommodity() + services.fillWithSomeTestLinearStates(10) +// services.consumeLinearStates(8) + + val criteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED, + contractStateTypes = setOf(FungibleAsset::class.java)) + val results = vaultSvc.queryBy>(criteria) + assertThat(results.states).hasSize(3) + } + } + + @Test + fun `unconsumed cash fungible assets`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) + services.fillWithSomeTestLinearStates(10) + + val criteria = VaultQueryCriteria(contractStateTypes = setOf(Cash.State::class.java)) // default is UNCONSUMED + val results = vaultSvc.queryBy(criteria) + assertThat(results.states).hasSize(3) + } + } + + @Test + fun `consumed cash fungible assets`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) +// services.consumeCash(2) + services.fillWithSomeTestLinearStates(10) +// services.consumeLinearStates(8) + + val criteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED) + val results = vaultSvc.queryBy(criteria) + assertThat(results.states).hasSize(1) + } + } + + @Test + fun `unconsumed linear heads`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) + services.fillWithSomeTestLinearStates(10) + + val criteria = VaultQueryCriteria(contractStateTypes = setOf(LinearState::class.java)) // default is UNCONSUMED + val results = vaultSvc.queryBy(criteria) + assertThat(results.states).hasSize(10) + } + } + + @Test + fun `consumed linear heads`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) + services.fillWithSomeTestLinearStates(10) +// services.consumeLinearStates(8) + + val criteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED, + contractStateTypes = setOf(LinearState::class.java)) + val results = vaultSvc.queryBy(criteria) + assertThat(results.states).hasSize(2) + } + } + + /** LinearState tests */ + + @Test + fun `unconsumed linear heads for linearId`() { + database.transaction { + + val issuedStates = services.fillWithSomeTestLinearStates(10) + + // DOCSTART VaultQueryExample8 + val linearIds = issuedStates.states.map { it.state.data.linearId }.toList() + val criteria = LinearStateQueryCriteria(linearId = listOf(linearIds.first(), linearIds.last())) + val results = vaultSvc.queryBy(criteria) + // DOCEND VaultQueryExample8 + assertThat(results.states).hasSize(2) + } + } + + @Test + fun `latest unconsumed linear heads for linearId`() { + database.transaction { + + val issuedStates = services.fillWithSomeTestLinearStates(2, UniqueIdentifier("TEST")) // create 2 states with same UID + services.fillWithSomeTestLinearStates(8) + + val linearIds = issuedStates.states.map { it.state.data.linearId }.toList() + val criteria = LinearStateQueryCriteria(linearId = listOf(linearIds.first()), + latestOnly = true) + val results = vaultSvc.queryBy(criteria) + assertThat(results.states).hasSize(1) + } + } + + @Test + fun `return chain of linear state for a given id`() { + database.transaction { + + val id = UniqueIdentifier("TEST") + services.fillWithSomeTestLinearStates(1, UniqueIdentifier("TEST")) +// services.processLinearState(id) // consume current and produce new state reference +// services.processLinearState(id) // consume current and produce new state reference +// services.processLinearState(id) // consume current and produce new state reference + + // should now have 1 UNCONSUMED & 3 CONSUMED state refs for Linear State with UniqueIdentifier("TEST") + // DOCSTART VaultQueryExample9 + val linearStateCriteria = LinearStateQueryCriteria(linearId = listOf(id)) + val vaultCriteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) + val sorting = Sort(setOf(Sort.SortColumn(VaultSchema.VaultLinearState::uuid.name, Sort.Direction.DESC))) + val results = vaultSvc.queryBy(linearStateCriteria.and(vaultCriteria), sorting) + // DOCEND VaultQueryExample9 + assertThat(results.states).hasSize(4) + } + } + + @Test + fun `DEPRECATED return linear states for a given id`() { + database.transaction { + + val linearUid = UniqueIdentifier("TEST") + services.fillWithSomeTestLinearStates(1, UniqueIdentifier("TEST")) +// services.processLinearState(id) // consume current and produce new state reference +// services.processLinearState(id) // consume current and produce new state reference +// services.processLinearState(id) // consume current and produce new state reference + + // should now have 1 UNCONSUMED & 3 CONSUMED state refs for Linear State with UniqueIdentifier("TEST") + + // DOCSTART VaultDeprecatedQueryExample1 + val states = vaultSvc.linearHeadsOfType().filter { it.key == linearUid } + // DOCEND VaultDeprecatedQueryExample1 + + assertThat(states).hasSize(4) + } + } + + @Test + fun `DEPRECATED return consumed linear states for a given id`() { + database.transaction { + + val linearUid = UniqueIdentifier("TEST") + services.fillWithSomeTestLinearStates(1, UniqueIdentifier("TEST")) +// services.processLinearState(id) // consume current and produce new state reference +// services.processLinearState(id) // consume current and produce new state reference +// services.processLinearState(id) // consume current and produce new state reference + + // should now have 1 UNCONSUMED & 3 CONSUMED state refs for Linear State with UniqueIdentifier("TEST") + + // DOCSTART VaultDeprecatedQueryExample2 + val states = vaultSvc.states(setOf(LinearState::class.java), + EnumSet.of(Vault.StateStatus.CONSUMED)).filter { it.state.data.linearId == linearUid } + // DOCEND VaultDeprecatedQueryExample2 + + assertThat(states).hasSize(3) + } + } + + @Test + fun `latest unconsumed linear heads for state refs`() { + database.transaction { + + val issuedStates = services.fillWithSomeTestLinearStates(2, UniqueIdentifier("TEST")) // create 2 states with same UID + services.fillWithSomeTestLinearStates(8) + val stateRefs = issuedStates.states.map { it.ref }.toList() + + val vaultCriteria = VaultQueryCriteria(stateRefs = listOf(stateRefs.first(), stateRefs.last())) + val linearStateCriteria = LinearStateQueryCriteria(latestOnly = true) + val results = vaultSvc.queryBy(vaultCriteria.and(linearStateCriteria)) + assertThat(results.states).hasSize(2) + } + } + + @Test + fun `unconsumed deals`() { + database.transaction { + + services.fillWithSomeTestDeals(listOf("123", "456", "789")) + + val criteria = LinearStateQueryCriteria() + val results = vaultSvc.queryBy(criteria) + assertThat(results.states).hasSize(3) + } + } + + @Test + fun `unconsumed deals for ref`() { + database.transaction { + + services.fillWithSomeTestDeals(listOf("123", "456", "789")) + + // DOCSTART VaultQueryExample10 + val criteria = LinearStateQueryCriteria(dealRef = listOf("456", "789")) + val results = vaultSvc.queryBy(criteria) + // DOCEND VaultQueryExample10 + + assertThat(results.states).hasSize(2) + } + } + + @Test + fun `latest unconsumed deals for ref`() { + database.transaction { + + services.fillWithSomeTestLinearStates(2, UniqueIdentifier("TEST")) + services.fillWithSomeTestDeals(listOf("456"), 3) // create 3 revisions with same ID + services.fillWithSomeTestDeals(listOf("123", "789")) + + val criteria = LinearStateQueryCriteria(dealRef = listOf("456"), latestOnly = true) + val results = vaultSvc.queryBy(criteria) + assertThat(results.states).hasSize(1) + } + } + + @Test + fun `latest unconsumed deals with party`() { + database.transaction { + + services.fillWithSomeTestLinearStates(2, UniqueIdentifier("TEST")) + services.fillWithSomeTestDeals(listOf("456"), 3) // specify party + services.fillWithSomeTestDeals(listOf("123", "789")) + + // DOCSTART VaultQueryExample11 + val criteria = LinearStateQueryCriteria(dealPartyName = listOf(MEGA_CORP.name, MINI_CORP.name)) + val results = vaultSvc.queryBy(criteria) + // DOCEND VaultQueryExample11 + + assertThat(results.states).hasSize(1) + } + } + + /** FungibleAsset tests */ + @Test + fun `unconsumed fungible assets of token type`() { + database.transaction { + + services.fillWithSomeTestLinearStates(10) + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) + services.fillWithSomeTestCash(100.POUNDS, DUMMY_NOTARY, 3, 3, Random(0L)) + services.fillWithSomeTestCash(100.SWISS_FRANCS, DUMMY_NOTARY, 3, 3, Random(0L)) + + val criteria = FungibleAssetQueryCriteria(tokenType = listOf(Currency::class.java)) + val results = vaultSvc.queryBy>(criteria) + assertThat(results.states).hasSize(9) + } + } + + @Test + fun `unconsumed fungible assets for single currency`() { + database.transaction { + + services.fillWithSomeTestLinearStates(10) + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) + services.fillWithSomeTestCash(100.POUNDS, DUMMY_NOTARY, 3, 3, Random(0L)) + services.fillWithSomeTestCash(100.SWISS_FRANCS, DUMMY_NOTARY, 3, 3, Random(0L)) + + // DOCSTART VaultQueryExample12 + val criteria = FungibleAssetQueryCriteria(tokenValue = listOf(USD.currencyCode)) + val results = vaultSvc.queryBy>(criteria) + // DOCEND VaultQueryExample12 + + assertThat(results.states).hasSize(3) + } + } + + @Test + fun `unconsumed fungible assets for single currency and quantity greater than`() { + database.transaction { + + services.fillWithSomeTestLinearStates(10) + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) + services.fillWithSomeTestCash(100.POUNDS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(50.POUNDS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(100.SWISS_FRANCS, DUMMY_NOTARY, 3, 3, Random(0L)) + + // DOCSTART VaultQueryExample13 + val criteria = FungibleAssetQueryCriteria(tokenValue = listOf(GBP.currencyCode), + quantity = LogicalExpression(this, Operator.GREATER_THAN, 50)) + val results = vaultSvc.queryBy(criteria) + // DOCEND VaultQueryExample13 + + assertThat(results.states).hasSize(1) + } + } + + @Test + fun `unconsumed fungible assets for several currencies`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) + services.fillWithSomeTestCash(100.POUNDS, DUMMY_NOTARY, 3, 3, Random(0L)) + services.fillWithSomeTestCash(100.SWISS_FRANCS, DUMMY_NOTARY, 3, 3, Random(0L)) + + val criteria = FungibleAssetQueryCriteria(tokenValue = listOf(CHF.currencyCode, GBP.currencyCode)) + val results = vaultSvc.queryBy>(criteria) + assertThat(results.states).hasSize(3) + } + } + + @Test + fun `unconsumed fungible assets for issuer party`() { + database.transaction { + + 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) + + // DOCSTART VaultQueryExample14 + val criteria = FungibleAssetQueryCriteria(issuerPartyName = listOf(BOC.name)) + val results = vaultSvc.queryBy>(criteria) + // DOCEND VaultQueryExample14 + + assertThat(results.states).hasSize(1) + } + } + + @Test + fun `unconsumed fungible assets for specific issuer party and refs`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (DUMMY_CASH_ISSUER)) + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(1)), issuerKey = BOC_KEY, ref = OpaqueBytes.of(1)) + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(2)), issuerKey = BOC_KEY, ref = OpaqueBytes.of(2)) + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(3)), issuerKey = BOC_KEY, ref = OpaqueBytes.of(3)) + + val criteria = FungibleAssetQueryCriteria(issuerPartyName = listOf(BOC.name), + issuerRef = listOf(BOC.ref(1).reference, BOC.ref(2).reference)) + val results = vaultSvc.queryBy>(criteria) + assertThat(results.states).hasSize(2) + } + } + + @Test + fun `unconsumed fungible assets with exit keys`() { + database.transaction { + + 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) + + // DOCSTART VaultQueryExample15 + val criteria = FungibleAssetQueryCriteria(exitKeyIdentity = listOf(DUMMY_CASH_ISSUER.party.toString())) + val results = vaultSvc.queryBy>(criteria) + // DOCEND VaultQueryExample15 + + assertThat(results.states).hasSize(1) + } + } + + @Test + fun `unconsumed fungible assets by owner`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 2, 2, Random(0L), issuedBy = (DUMMY_CASH_ISSUER)) + // issue some cash to BOB + // issue some cash to ALICE + + val criteria = FungibleAssetQueryCriteria(ownerIdentity = listOf(BOB.name, ALICE.name)) + val results = vaultSvc.queryBy>(criteria) + assertThat(results.states).hasSize(1) + } + } + + /** Vault Custom Query tests */ + + // specifying Query on Commercial Paper contract state attributes + @Test + fun `commercial paper custom query`() { + database.transaction { + + // MegaCorp™ issues $10,000 of commercial paper, to mature in 30 days, owned by itself. + val faceValue = 10000.DOLLARS `issued by` DUMMY_CASH_ISSUER + val issuance = MEGA_CORP.ref(1) + val commercialPaper = + CommercialPaper().generateIssue(issuance, faceValue, TEST_TX_TIME + 30.days, DUMMY_NOTARY).apply { + setTime(TEST_TX_TIME, 30.seconds) + signWith(MEGA_CORP_KEY) + signWith(DUMMY_NOTARY_KEY) + }.toSignedTransaction() + services.recordTransactions(commercialPaper) + + val ccyIndex = LogicalExpression(PersistentCommercialPaperState::currency, Operator.EQUAL, USD.currencyCode) + val maturityIndex = LogicalExpression(PersistentCommercialPaperState::maturity, Operator.GREATER_THAN_OR_EQUAL, TEST_TX_TIME + 30.days) + val faceValueIndex = LogicalExpression(PersistentCommercialPaperState::faceValue, Operator.GREATER_THAN_OR_EQUAL, 10000) + + val criteria = VaultCustomQueryCriteria(maturityIndex.and(faceValueIndex).or(ccyIndex)) + val result = vaultSvc.queryBy(criteria) + + assertThat(result.states).hasSize(1) + assertThat(result.statesMetadata).hasSize(1) + } + } + + /** Chaining together different Query Criteria tests**/ + + // specifying Query on Cash contract state attributes + @Test + fun `all cash states with amount of currency greater or equal than`() { + + database.transaction { + + services.fillWithSomeTestCash(100.POUNDS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(10.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + services.fillWithSomeTestCash(1.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L)) + + // DOCSTART VaultQueryExample16 + val generalCriteria = VaultQueryCriteria(Vault.StateStatus.ALL) + + val currencyIndex = LogicalExpression(PersistentCashState::currency, Operator.EQUAL, USD.currencyCode) + val quantityIndex = LogicalExpression(PersistentCashState::pennies, Operator.GREATER_THAN_OR_EQUAL, 10) + val customCriteria = VaultCustomQueryCriteria(currencyIndex.and(quantityIndex)) + + val criteria = generalCriteria.and(customCriteria) + val results = vaultSvc.queryBy(criteria) + // DOCEND VaultQueryExample16 + + assertThat(results.states).hasSize(2) + } + } + + // specifying Query on Linear state attributes + @Test + fun `consumed linear heads for linearId between two timestamps`() { + database.transaction { + val issuedStates = services.fillWithSomeTestLinearStates(10) + val externalIds = issuedStates.states.map { it.state.data.linearId }.map { it.externalId }[0] + val uuids = issuedStates.states.map { it.state.data.linearId }.map { it.id }[1] + + val start = TEST_TX_TIME + val end = TEST_TX_TIME.plus(30, ChronoUnit.DAYS) + val recordedBetweenExpression = LogicalExpression(TimeInstantType.RECORDED, Operator.BETWEEN, arrayOf(start, end)) + val basicCriteria = VaultQueryCriteria(timeCondition = recordedBetweenExpression) + + val linearIdsExpression = LogicalExpression(VaultLinearStateEntity::externalId, Operator.IN, externalIds) + val linearIdCondition = LogicalExpression(VaultLinearStateEntity::uuid, Operator.EQUAL, uuids) + val customIndexCriteria = VaultCustomQueryCriteria(linearIdsExpression.or(linearIdCondition)) + + val criteria = basicCriteria.and(customIndexCriteria) + val results = vaultSvc.queryBy(criteria) + + assertThat(results.states).hasSize(2) + } + } + + /** + * USE CASE demonstrations (outside of mainline Corda) + * + * 1) Template / Tutorial CorDapp service using Vault API Custom Query to access attributes of IOU State + * 2) Template / Tutorial Flow using a JDBC session to execute a custom query + * 3) Template / Tutorial CorDapp service query extension executing Named Queries via JPA + * 4) Advanced pagination queries using Spring Data (and/or Hibernate/JPQL) + */ +} \ No newline at end of file