mirror of
https://github.com/corda/corda.git
synced 2025-06-22 00:57:21 +00:00
Merge pull request #1184 from corda/mnesbit-generic-generatespend
Move generateSpend onto Cash contract. Add fully generic replacement.
This commit is contained in:
@ -5,18 +5,13 @@ import net.corda.core.concurrent.CordaFuture
|
|||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.flows.FlowException
|
import net.corda.core.flows.FlowException
|
||||||
import net.corda.core.identity.AbstractParty
|
import net.corda.core.node.services.vault.QueryCriteria
|
||||||
import net.corda.core.identity.Party
|
|
||||||
import net.corda.core.messaging.DataFeed
|
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.toFuture
|
import net.corda.core.toFuture
|
||||||
import net.corda.core.transactions.CoreTransaction
|
import net.corda.core.transactions.CoreTransaction
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
|
||||||
import net.corda.core.utilities.NonEmptySet
|
import net.corda.core.utilities.NonEmptySet
|
||||||
import net.corda.core.utilities.OpaqueBytes
|
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.subjects.PublishSubject
|
import rx.subjects.PublishSubject
|
||||||
import java.security.PublicKey
|
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@ -224,29 +219,7 @@ interface VaultService {
|
|||||||
|
|
||||||
fun getTransactionNotes(txnId: SecureHash): Iterable<String>
|
fun getTransactionNotes(txnId: SecureHash): Iterable<String>
|
||||||
|
|
||||||
/**
|
// DOCEND VaultStatesQuery
|
||||||
* Generate a transaction that moves an amount of currency to the given pubkey.
|
|
||||||
*
|
|
||||||
* Note: an [Amount] of [Currency] is only fungible for a given Issuer Party within a [FungibleAsset]
|
|
||||||
*
|
|
||||||
* @param tx A builder, which may contain inputs, outputs and commands already. The relevant components needed
|
|
||||||
* to move the cash will be added on top.
|
|
||||||
* @param amount How much currency to send.
|
|
||||||
* @param to a key of the recipient.
|
|
||||||
* @param onlyFromParties if non-null, the asset states will be filtered to only include those issued by the set
|
|
||||||
* of given parties. This can be useful if the party you're trying to pay has expectations
|
|
||||||
* about which type of asset claims they are willing to accept.
|
|
||||||
* @return A [Pair] of the same transaction builder passed in as [tx], and the list of keys that need to sign
|
|
||||||
* the resulting transaction for it to be valid.
|
|
||||||
* @throws InsufficientBalanceException when a cash spending transaction fails because
|
|
||||||
* there is insufficient quantity for a given currency (and optionally set of Issuer Parties).
|
|
||||||
*/
|
|
||||||
@Throws(InsufficientBalanceException::class)
|
|
||||||
@Suspendable
|
|
||||||
fun generateSpend(tx: TransactionBuilder,
|
|
||||||
amount: Amount<Currency>,
|
|
||||||
to: AbstractParty,
|
|
||||||
onlyFromParties: Set<AbstractParty>? = null): Pair<TransactionBuilder, List<PublicKey>>
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Soft locking is used to prevent multiple transactions trying to use the same output simultaneously.
|
* Soft locking is used to prevent multiple transactions trying to use the same output simultaneously.
|
||||||
@ -257,7 +230,10 @@ interface VaultService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Reserve a set of [StateRef] for a given [UUID] unique identifier.
|
* Reserve a set of [StateRef] for a given [UUID] unique identifier.
|
||||||
* Typically, the unique identifier will refer to a Flow lockId associated with a [Transaction] in an in-flight flow.
|
* Typically, the unique identifier will refer to a [FlowLogic.runId.uuid] associated with an in-flight flow.
|
||||||
|
* In this case if the flow terminates the locks will automatically be freed, even if there is an error.
|
||||||
|
* However, the user can specify their own [UUID] and manage this manually, possibly across the lifetime of multiple flows,
|
||||||
|
* or from other thread contexts e.g. [CordaService] instances.
|
||||||
* In the case of coin selection, soft locks are automatically taken upon gathering relevant unconsumed input refs.
|
* In the case of coin selection, soft locks are automatically taken upon gathering relevant unconsumed input refs.
|
||||||
*
|
*
|
||||||
* @throws [StatesNotAvailableException] when not possible to softLock all of requested [StateRef]
|
* @throws [StatesNotAvailableException] when not possible to softLock all of requested [StateRef]
|
||||||
@ -273,21 +249,32 @@ interface VaultService {
|
|||||||
* are consumed as part of cash spending.
|
* are consumed as part of cash spending.
|
||||||
*/
|
*/
|
||||||
fun softLockRelease(lockId: UUID, stateRefs: NonEmptySet<StateRef>? = null)
|
fun softLockRelease(lockId: UUID, stateRefs: NonEmptySet<StateRef>? = null)
|
||||||
|
|
||||||
// DOCEND SoftLockAPI
|
// DOCEND SoftLockAPI
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO: this function should be private to the vault, but currently Cash Exit functionality
|
* Helper function to combine using [VaultQueryService] calls to determine spendable states and soft locking them.
|
||||||
* is implemented in a separate module (finance) and requires access to it.
|
* Currently performance will be worse than for the hand optimised version in `Cash.unconsumedCashStatesForSpending`
|
||||||
|
* However, this is fully generic and can operate with custom [FungibleAsset] states.
|
||||||
|
* @param lockId The [FlowLogic.runId.uuid] of the current flow used to soft lock the states.
|
||||||
|
* @param eligibleStatesQuery A custom query object that selects down to the appropriate subset of all states of the
|
||||||
|
* [contractType]. e.g. by selecting on account, issuer, etc. The query is internally augmented with the UNCONSUMED,
|
||||||
|
* soft lock and contract type requirements.
|
||||||
|
* @param amount The required amount of the asset, but with the issuer stripped off.
|
||||||
|
* It is assumed that compatible issuer states will be filtered out by the [eligibleStatesQuery].
|
||||||
|
* @param contractType class type of the result set.
|
||||||
|
* @return Returns a locked subset of the [eligibleStatesQuery] sufficient to satisfy the requested amount,
|
||||||
|
* or else an empty list and no change in the stored lock states when their are insufficient resources available.
|
||||||
*/
|
*/
|
||||||
@Suspendable
|
@Suspendable
|
||||||
fun <T : ContractState> unconsumedStatesForSpending(amount: Amount<Currency>,
|
@Throws(StatesNotAvailableException::class)
|
||||||
onlyFromIssuerParties: Set<AbstractParty>? = null,
|
fun <T : FungibleAsset<U>, U : Any> tryLockFungibleStatesForSpending(lockId: UUID,
|
||||||
notary: Party? = null,
|
eligibleStatesQuery: QueryCriteria,
|
||||||
lockId: UUID,
|
amount: Amount<U>,
|
||||||
withIssuerRefs: Set<OpaqueBytes>? = null): List<StateAndRef<T>>
|
contractType: Class<out T>): List<StateAndRef<T>>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class StatesNotAvailableException(override val message: String?, override val cause: Throwable? = null) : FlowException(message, cause) {
|
class StatesNotAvailableException(override val message: String?, override val cause: Throwable? = null) : FlowException(message, cause) {
|
||||||
override fun toString() = "Soft locking error: $message"
|
override fun toString() = "Soft locking error: $message"
|
||||||
}
|
}
|
@ -60,6 +60,10 @@ UNRELEASED
|
|||||||
* ``Cordformation`` adds a ``corda`` and ``cordaRuntime`` configuration to projects which cordapp developers should
|
* ``Cordformation`` adds a ``corda`` and ``cordaRuntime`` configuration to projects which cordapp developers should
|
||||||
use to exclude core Corda JARs from being built into Cordapp fat JARs.
|
use to exclude core Corda JARs from being built into Cordapp fat JARs.
|
||||||
|
|
||||||
|
* Move the original ``Cash`` specific ``generateSpend`` and ``unconsumedStatesForSpending`` methods from ``:core``
|
||||||
|
and onto ``Cash`` contract in the ``:finance`` module. Provide a genuinely generic ``tryLockFungibleStatesForSpending``
|
||||||
|
on ``VaultService``, which in future could be optimised for performance.
|
||||||
|
|
||||||
.. Milestone 15:
|
.. Milestone 15:
|
||||||
|
|
||||||
* Vault Query fix: filter by multiple issuer names in ``FungibleAssetQueryCriteria``
|
* Vault Query fix: filter by multiple issuer names in ``FungibleAssetQueryCriteria``
|
||||||
|
@ -5,6 +5,7 @@ import net.corda.contracts.asset.Cash
|
|||||||
import net.corda.core.contracts.Amount
|
import net.corda.core.contracts.Amount
|
||||||
import net.corda.core.contracts.Issued
|
import net.corda.core.contracts.Issued
|
||||||
import net.corda.core.contracts.StateAndRef
|
import net.corda.core.contracts.StateAndRef
|
||||||
|
import net.corda.core.contracts.withoutIssuer
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.crypto.TransactionSignature
|
import net.corda.core.crypto.TransactionSignature
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
@ -13,7 +14,6 @@ import net.corda.core.flows.InitiatingFlow
|
|||||||
import net.corda.core.flows.*
|
import net.corda.core.flows.*
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.node.ServiceHub
|
import net.corda.core.node.ServiceHub
|
||||||
import net.corda.core.node.services.queryBy
|
|
||||||
import net.corda.core.node.services.vault.QueryCriteria
|
import net.corda.core.node.services.vault.QueryCriteria
|
||||||
import net.corda.core.node.services.vault.builder
|
import net.corda.core.node.services.vault.builder
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
@ -31,9 +31,10 @@ private data class FxRequest(val tradeId: String,
|
|||||||
val notary: Party? = null)
|
val notary: Party? = null)
|
||||||
|
|
||||||
// DOCSTART 1
|
// DOCSTART 1
|
||||||
// This is equivalent to the VaultService.generateSpend
|
// This is equivalent to the Cash.generateSpend
|
||||||
// Which is brought here to make the filtering logic more visible in the example
|
// Which is brought here to make the filtering logic more visible in the example
|
||||||
private fun gatherOurInputs(serviceHub: ServiceHub,
|
private fun gatherOurInputs(serviceHub: ServiceHub,
|
||||||
|
lockId: UUID,
|
||||||
amountRequired: Amount<Issued<Currency>>,
|
amountRequired: Amount<Issued<Currency>>,
|
||||||
notary: Party?): Pair<List<StateAndRef<Cash.State>>, Long> {
|
notary: Party?): Pair<List<StateAndRef<Cash.State>>, Long> {
|
||||||
// extract our identity for convenience
|
// extract our identity for convenience
|
||||||
@ -41,34 +42,25 @@ private fun gatherOurInputs(serviceHub: ServiceHub,
|
|||||||
val ourParties = ourKeys.map { serviceHub.identityService.partyFromKey(it) ?: throw IllegalStateException("Unable to resolve party from key") }
|
val ourParties = ourKeys.map { serviceHub.identityService.partyFromKey(it) ?: throw IllegalStateException("Unable to resolve party from key") }
|
||||||
val fungibleCriteria = QueryCriteria.FungibleAssetQueryCriteria(owner = ourParties)
|
val fungibleCriteria = QueryCriteria.FungibleAssetQueryCriteria(owner = ourParties)
|
||||||
|
|
||||||
|
val notaryName = if (notary != null) notary.name else serviceHub.networkMapCache.getAnyNotary()!!.name
|
||||||
|
val vaultCriteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(notaryName = listOf(notaryName))
|
||||||
|
|
||||||
val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.equal(amountRequired.token.product.currencyCode) }
|
val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.equal(amountRequired.token.product.currencyCode) }
|
||||||
val cashCriteria = QueryCriteria.VaultCustomQueryCriteria(logicalExpression)
|
val cashCriteria = QueryCriteria.VaultCustomQueryCriteria(logicalExpression)
|
||||||
|
|
||||||
// Collect cash type inputs
|
val fullCriteria = fungibleCriteria.and(vaultCriteria).and(cashCriteria)
|
||||||
val suitableCashStates = serviceHub.vaultQueryService.queryBy<Cash.State>(fungibleCriteria.and(cashCriteria)).states
|
|
||||||
require(!suitableCashStates.isEmpty()) { "Insufficient funds" }
|
|
||||||
|
|
||||||
var remaining = amountRequired.quantity
|
val eligibleStates = serviceHub.vaultService.tryLockFungibleStatesForSpending<Cash.State, Currency>(lockId, fullCriteria, amountRequired.withoutIssuer(), Cash.State::class.java)
|
||||||
// We will need all of the inputs to be on the same notary.
|
|
||||||
// For simplicity we just filter on the first notary encountered
|
|
||||||
// A production quality flow would need to migrate notary if the
|
|
||||||
// the amounts were not sufficient in any one notary
|
|
||||||
val sourceNotary: Party = notary ?: suitableCashStates.first().state.notary
|
|
||||||
|
|
||||||
val inputsList = mutableListOf<StateAndRef<Cash.State>>()
|
check(eligibleStates.isNotEmpty()) { "Insufficient funds" }
|
||||||
// Iterate over filtered cash states to gather enough to pay
|
val amount = eligibleStates.fold(0L) { tot, x -> tot + x.state.data.amount.quantity }
|
||||||
for (cash in suitableCashStates.filter { it.state.notary == sourceNotary }) {
|
val change = amount - amountRequired.quantity
|
||||||
inputsList += cash
|
|
||||||
if (remaining <= cash.state.data.amount.quantity) {
|
return Pair(eligibleStates, change)
|
||||||
return Pair(inputsList, cash.state.data.amount.quantity - remaining)
|
|
||||||
}
|
|
||||||
remaining -= cash.state.data.amount.quantity
|
|
||||||
}
|
|
||||||
throw IllegalStateException("Insufficient funds")
|
|
||||||
}
|
}
|
||||||
// DOCEND 1
|
// DOCEND 1
|
||||||
|
|
||||||
private fun prepareOurInputsAndOutputs(serviceHub: ServiceHub, request: FxRequest): Pair<List<StateAndRef<Cash.State>>, List<Cash.State>> {
|
private fun prepareOurInputsAndOutputs(serviceHub: ServiceHub, lockId: UUID, request: FxRequest): Pair<List<StateAndRef<Cash.State>>, List<Cash.State>> {
|
||||||
// Create amount with correct issuer details
|
// Create amount with correct issuer details
|
||||||
val sellAmount = request.amount
|
val sellAmount = request.amount
|
||||||
|
|
||||||
@ -78,7 +70,7 @@ private fun prepareOurInputsAndOutputs(serviceHub: ServiceHub, request: FxReques
|
|||||||
// we will use query manually in the helper function below.
|
// we will use query manually in the helper function below.
|
||||||
// Putting this into a non-suspendable function also prevents issues when
|
// Putting this into a non-suspendable function also prevents issues when
|
||||||
// the flow is suspended.
|
// the flow is suspended.
|
||||||
val (inputs, residual) = gatherOurInputs(serviceHub, sellAmount, request.notary)
|
val (inputs, residual) = gatherOurInputs(serviceHub, lockId, sellAmount, request.notary)
|
||||||
|
|
||||||
// Build and an output state for the counterparty
|
// Build and an output state for the counterparty
|
||||||
val transferedFundsOutput = Cash.State(sellAmount, request.counterparty)
|
val transferedFundsOutput = Cash.State(sellAmount, request.counterparty)
|
||||||
@ -119,7 +111,7 @@ class ForeignExchangeFlow(val tradeId: String,
|
|||||||
} else throw IllegalArgumentException("Our identity must be one of the parties in the trade.")
|
} else throw IllegalArgumentException("Our identity must be one of the parties in the trade.")
|
||||||
|
|
||||||
// Call the helper method to identify suitable inputs and make the outputs
|
// Call the helper method to identify suitable inputs and make the outputs
|
||||||
val (outInputStates, ourOutputStates) = prepareOurInputsAndOutputs(serviceHub, localRequest)
|
val (outInputStates, ourOutputStates) = prepareOurInputsAndOutputs(serviceHub, runId.uuid, localRequest)
|
||||||
|
|
||||||
// identify the notary for our states
|
// identify the notary for our states
|
||||||
val notary = outInputStates.first().state.notary
|
val notary = outInputStates.first().state.notary
|
||||||
@ -231,7 +223,7 @@ class ForeignExchangeRemoteFlow(val source: Party) : FlowLogic<Unit>() {
|
|||||||
// we will use query manually in the helper function below.
|
// we will use query manually in the helper function below.
|
||||||
// Putting this into a non-suspendable function also prevent issues when
|
// Putting this into a non-suspendable function also prevent issues when
|
||||||
// the flow is suspended.
|
// the flow is suspended.
|
||||||
val (ourInputState, ourOutputState) = prepareOurInputsAndOutputs(serviceHub, request)
|
val (ourInputState, ourOutputState) = prepareOurInputsAndOutputs(serviceHub, runId.uuid, request)
|
||||||
|
|
||||||
// Send back our proposed states and await the full transaction to verify
|
// Send back our proposed states and await the full transaction to verify
|
||||||
val ourKey = serviceHub.keyManagementService.filterMyKeys(ourInputState.flatMap { it.state.data.participants }.map { it.owningKey }).single()
|
val ourKey = serviceHub.keyManagementService.filterMyKeys(ourInputState.flatMap { it.state.data.participants }.map { it.owningKey }).single()
|
||||||
|
@ -279,7 +279,7 @@ This code is longer but no more complicated. Here are some things to pay attenti
|
|||||||
|
|
||||||
1. We do some sanity checking on the proposed trade transaction received from the seller to ensure we're being offered
|
1. We do some sanity checking on the proposed trade transaction received from the seller to ensure we're being offered
|
||||||
what we expected to be offered.
|
what we expected to be offered.
|
||||||
2. We create a cash spend using ``VaultService.generateSpend``. You can read the vault documentation to learn more about this.
|
2. We create a cash spend using ``Cash.generateSpend``. You can read the vault documentation to learn more about this.
|
||||||
3. We access the *service hub* as needed to access things that are transient and may change or be recreated
|
3. We access the *service hub* as needed to access things that are transient and may change or be recreated
|
||||||
whilst a flow is suspended, such as the wallet or the network map.
|
whilst a flow is suspended, such as the wallet or the network map.
|
||||||
4. We call ``CollectSignaturesFlow`` as a subflow to send the unfinished, still-invalid transaction to the seller so
|
4. We call ``CollectSignaturesFlow`` as a subflow to send the unfinished, still-invalid transaction to the seller so
|
||||||
|
@ -116,11 +116,12 @@ standard ``CashState`` in the ``:financial`` Gradle module. The Cash
|
|||||||
contract uses ``FungibleAsset`` states to model holdings of
|
contract uses ``FungibleAsset`` states to model holdings of
|
||||||
interchangeable assets and allow the split/merge and summing of
|
interchangeable assets and allow the split/merge and summing of
|
||||||
states to meet a contractual obligation. We would normally use the
|
states to meet a contractual obligation. We would normally use the
|
||||||
``generateSpend`` method on the ``VaultService`` to gather the required
|
``Cash.generateSpend`` method to gather the required
|
||||||
amount of cash into a ``TransactionBuilder``, set the outputs and move
|
amount of cash into a ``TransactionBuilder``, set the outputs and move
|
||||||
command. However, to elucidate more clearly example flow code is shown
|
command. However, to elucidate more clearly example flow code is shown
|
||||||
here that will manually carry out the inputs queries by specifying relevant
|
here that will manually carry out the inputs queries by specifying relevant
|
||||||
query criteria filters to the ``queryBy`` method of the ``VaultQueryService``.
|
query criteria filters to the ``tryLockFungibleStatesForSpending`` method
|
||||||
|
of the ``VaultService``.
|
||||||
|
|
||||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt
|
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt
|
||||||
:language: kotlin
|
:language: kotlin
|
||||||
|
@ -680,9 +680,9 @@ Finally, we can do redemption.
|
|||||||
.. sourcecode:: kotlin
|
.. sourcecode:: kotlin
|
||||||
|
|
||||||
@Throws(InsufficientBalanceException::class)
|
@Throws(InsufficientBalanceException::class)
|
||||||
fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef<State>, vault: VaultService) {
|
fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef<State>, services: ServiceHub) {
|
||||||
// Add the cash movement using the states in our vault.
|
// Add the cash movement using the states in our vault.
|
||||||
vault.generateSpend(tx, paper.state.data.faceValue.withoutIssuer(), paper.state.data.owner)
|
Cash.generateSpend(services, tx, paper.state.data.faceValue.withoutIssuer(), paper.state.data.owner)
|
||||||
tx.addInputState(paper)
|
tx.addInputState(paper)
|
||||||
tx.addCommand(Command(Commands.Redeem(), paper.state.data.owner.owningKey))
|
tx.addCommand(Command(Commands.Redeem(), paper.state.data.owner.owningKey))
|
||||||
}
|
}
|
||||||
@ -698,7 +698,7 @@ from the issuer of the commercial paper to the current owner. If we don't have e
|
|||||||
an exception is thrown. Then we add the paper itself as an input, but, not an output (as we wish to remove it
|
an exception is thrown. Then we add the paper itself as an input, but, not an output (as we wish to remove it
|
||||||
from the ledger). Finally, we add a Redeem command that should be signed by the owner of the commercial paper.
|
from the ledger). Finally, we add a Redeem command that should be signed by the owner of the commercial paper.
|
||||||
|
|
||||||
.. warning:: The amount we pass to the ``generateSpend`` function has to be treated first with ``withoutIssuer``.
|
.. warning:: The amount we pass to the ``Cash.generateSpend`` function has to be treated first with ``withoutIssuer``.
|
||||||
This reflects the fact that the way we handle issuer constraints is still evolving; the commercial paper
|
This reflects the fact that the way we handle issuer constraints is still evolving; the commercial paper
|
||||||
contract requires payment in the form of a currency issued by a specific party (e.g. the central bank,
|
contract requires payment in the form of a currency issued by a specific party (e.g. the central bank,
|
||||||
or the issuers own bank perhaps). But the vault wants to assemble spend transactions using cash states from
|
or the issuers own bank perhaps). But the vault wants to assemble spend transactions using cash states from
|
||||||
|
@ -5,6 +5,7 @@ import com.google.common.collect.ImmutableList;
|
|||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
import kotlin.Pair;
|
import kotlin.Pair;
|
||||||
import kotlin.Unit;
|
import kotlin.Unit;
|
||||||
|
import net.corda.contracts.asset.Cash;
|
||||||
import net.corda.contracts.asset.CashKt;
|
import net.corda.contracts.asset.CashKt;
|
||||||
import net.corda.core.contracts.*;
|
import net.corda.core.contracts.*;
|
||||||
import net.corda.core.crypto.SecureHash;
|
import net.corda.core.crypto.SecureHash;
|
||||||
@ -12,15 +13,14 @@ import net.corda.core.crypto.testing.NullPublicKey;
|
|||||||
import net.corda.core.identity.AbstractParty;
|
import net.corda.core.identity.AbstractParty;
|
||||||
import net.corda.core.identity.AnonymousParty;
|
import net.corda.core.identity.AnonymousParty;
|
||||||
import net.corda.core.identity.Party;
|
import net.corda.core.identity.Party;
|
||||||
import net.corda.core.node.services.VaultService;
|
import net.corda.core.node.ServiceHub;
|
||||||
import net.corda.core.transactions.LedgerTransaction;
|
import net.corda.core.transactions.LedgerTransaction;
|
||||||
import net.corda.core.transactions.TransactionBuilder;
|
import net.corda.core.transactions.TransactionBuilder;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Currency;
|
import java.util.*;
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static net.corda.core.contracts.ContractsDSL.requireSingleCommand;
|
import static net.corda.core.contracts.ContractsDSL.requireSingleCommand;
|
||||||
@ -255,8 +255,8 @@ public class JavaCommercialPaper implements Contract {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
public void generateRedeem(TransactionBuilder tx, StateAndRef<State> paper, VaultService vault) throws InsufficientBalanceException {
|
public void generateRedeem(TransactionBuilder tx, StateAndRef<State> paper, ServiceHub services) throws InsufficientBalanceException {
|
||||||
vault.generateSpend(tx, Structures.withoutIssuer(paper.getState().getData().getFaceValue()), paper.getState().getData().getOwner(), null);
|
Cash.generateSpend(services, tx, Structures.withoutIssuer(paper.getState().getData().getFaceValue()), paper.getState().getData().getOwner(), Collections.EMPTY_SET);
|
||||||
tx.addInputState(paper);
|
tx.addInputState(paper);
|
||||||
tx.addCommand(new Command<>(new Commands.Redeem(), paper.getState().getData().getOwner().getOwningKey()));
|
tx.addCommand(new Command<>(new Commands.Redeem(), paper.getState().getData().getOwner().getOwningKey()));
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package net.corda.contracts
|
package net.corda.contracts
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
|
import net.corda.contracts.asset.Cash
|
||||||
import net.corda.contracts.asset.sumCashBy
|
import net.corda.contracts.asset.sumCashBy
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
@ -9,7 +10,7 @@ import net.corda.core.crypto.toBase58String
|
|||||||
import net.corda.core.identity.AbstractParty
|
import net.corda.core.identity.AbstractParty
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.internal.Emoji
|
import net.corda.core.internal.Emoji
|
||||||
import net.corda.core.node.services.VaultService
|
import net.corda.core.node.ServiceHub
|
||||||
import net.corda.core.schemas.MappedSchema
|
import net.corda.core.schemas.MappedSchema
|
||||||
import net.corda.core.schemas.PersistentState
|
import net.corda.core.schemas.PersistentState
|
||||||
import net.corda.core.schemas.QueryableState
|
import net.corda.core.schemas.QueryableState
|
||||||
@ -185,9 +186,9 @@ class CommercialPaper : Contract {
|
|||||||
*/
|
*/
|
||||||
@Throws(InsufficientBalanceException::class)
|
@Throws(InsufficientBalanceException::class)
|
||||||
@Suspendable
|
@Suspendable
|
||||||
fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef<State>, vault: VaultService) {
|
fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef<State>, services: ServiceHub) {
|
||||||
// Add the cash movement using the states in our vault.
|
// Add the cash movement using the states in our vault.
|
||||||
vault.generateSpend(tx, paper.state.data.faceValue.withoutIssuer(), paper.state.data.owner)
|
Cash.generateSpend(services, tx, paper.state.data.faceValue.withoutIssuer(), paper.state.data.owner)
|
||||||
tx.addInputState(paper)
|
tx.addInputState(paper)
|
||||||
tx.addCommand(Commands.Redeem(), paper.state.data.owner.owningKey)
|
tx.addCommand(Commands.Redeem(), paper.state.data.owner.owningKey)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package net.corda.contracts.asset
|
package net.corda.contracts.asset
|
||||||
|
|
||||||
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
|
import co.paralleluniverse.strands.Strand
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.crypto.entropyToKeyPair
|
import net.corda.core.crypto.entropyToKeyPair
|
||||||
@ -9,16 +11,27 @@ import net.corda.core.crypto.toBase58String
|
|||||||
import net.corda.core.identity.AbstractParty
|
import net.corda.core.identity.AbstractParty
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.internal.Emoji
|
import net.corda.core.internal.Emoji
|
||||||
|
import net.corda.core.node.ServiceHub
|
||||||
|
import net.corda.core.node.services.StatesNotAvailableException
|
||||||
import net.corda.core.schemas.MappedSchema
|
import net.corda.core.schemas.MappedSchema
|
||||||
import net.corda.core.schemas.PersistentState
|
import net.corda.core.schemas.PersistentState
|
||||||
import net.corda.core.schemas.QueryableState
|
import net.corda.core.schemas.QueryableState
|
||||||
|
import net.corda.core.serialization.SerializationDefaults
|
||||||
|
import net.corda.core.serialization.deserialize
|
||||||
import net.corda.core.transactions.LedgerTransaction
|
import net.corda.core.transactions.LedgerTransaction
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
|
import net.corda.core.utilities.OpaqueBytes
|
||||||
|
import net.corda.core.utilities.toHexString
|
||||||
|
import net.corda.core.utilities.toNonEmptySet
|
||||||
|
import net.corda.core.utilities.trace
|
||||||
import net.corda.schemas.CashSchemaV1
|
import net.corda.schemas.CashSchemaV1
|
||||||
import org.bouncycastle.asn1.x500.X500Name
|
import org.bouncycastle.asn1.x500.X500Name
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
|
import java.sql.SQLException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import java.util.concurrent.locks.ReentrantLock
|
||||||
|
import kotlin.concurrent.withLock
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
@ -212,6 +225,163 @@ class Cash : OnLedgerAsset<Currency, Cash.Commands, Cash.State>() {
|
|||||||
"there is only a single issue command" using (cashCommands.count() == 1)
|
"there is only a single issue command" using (cashCommands.count() == 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// coin selection retry loop counter, sleep (msecs) and lock for selecting states
|
||||||
|
private val MAX_RETRIES = 5
|
||||||
|
private val RETRY_SLEEP = 100
|
||||||
|
private val spendLock: ReentrantLock = ReentrantLock()
|
||||||
|
/**
|
||||||
|
* Generate a transaction that moves an amount of currency to the given pubkey.
|
||||||
|
*
|
||||||
|
* Note: an [Amount] of [Currency] is only fungible for a given Issuer Party within a [FungibleAsset]
|
||||||
|
*
|
||||||
|
* @param services The [ServiceHub] to provide access to the database session.
|
||||||
|
* @param tx A builder, which may contain inputs, outputs and commands already. The relevant components needed
|
||||||
|
* to move the cash will be added on top.
|
||||||
|
* @param amount How much currency to send.
|
||||||
|
* @param to a key of the recipient.
|
||||||
|
* @param onlyFromParties if non-null, the asset states will be filtered to only include those issued by the set
|
||||||
|
* of given parties. This can be useful if the party you're trying to pay has expectations
|
||||||
|
* about which type of asset claims they are willing to accept.
|
||||||
|
* @return A [Pair] of the same transaction builder passed in as [tx], and the list of keys that need to sign
|
||||||
|
* the resulting transaction for it to be valid.
|
||||||
|
* @throws InsufficientBalanceException when a cash spending transaction fails because
|
||||||
|
* there is insufficient quantity for a given currency (and optionally set of Issuer Parties).
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
@Throws(InsufficientBalanceException::class)
|
||||||
|
@Suspendable
|
||||||
|
fun generateSpend(services: ServiceHub,
|
||||||
|
tx: TransactionBuilder,
|
||||||
|
amount: Amount<Currency>,
|
||||||
|
to: AbstractParty,
|
||||||
|
onlyFromParties: Set<AbstractParty> = emptySet()): Pair<TransactionBuilder, List<PublicKey>> {
|
||||||
|
|
||||||
|
fun deriveState(txState: TransactionState<Cash.State>, amt: Amount<Issued<Currency>>, owner: AbstractParty)
|
||||||
|
= txState.copy(data = txState.data.copy(amount = amt, owner = owner))
|
||||||
|
|
||||||
|
// Retrieve unspent and unlocked cash states that meet our spending criteria.
|
||||||
|
val acceptableCoins = Cash.unconsumedCashStatesForSpending(services, amount, onlyFromParties, tx.notary, tx.lockId)
|
||||||
|
return OnLedgerAsset.generateSpend(tx, amount, to, acceptableCoins,
|
||||||
|
{ state, quantity, owner -> deriveState(state, quantity, owner) },
|
||||||
|
{ Cash().generateMoveCommand() })
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An optimised query to gather Cash states that are available and retry if they are temporarily unavailable.
|
||||||
|
* @param services The service hub to allow access to the database session
|
||||||
|
* @param amount The amount of currency desired (ignoring issues, but specifying the currency)
|
||||||
|
* @param onlyFromIssuerParties If empty the operation ignores the specifics of the issuer,
|
||||||
|
* otherwise the set of eligible states wil be filtered to only include those from these issuers.
|
||||||
|
* @param notary If null the notary source is ignored, if specified then only states marked
|
||||||
|
* with this notary are included.
|
||||||
|
* @param lockId The [FlowLogic.runId.uuid] of the flow, which is used to soft reserve the states.
|
||||||
|
* Also, previous outputs of the flow will be eligible as they are implicitly locked with this id until the flow completes.
|
||||||
|
* @param withIssuerRefs If not empty the specific set of issuer references to match against.
|
||||||
|
* @return The matching states that were found. If sufficient funds were found these will be locked,
|
||||||
|
* otherwise what is available is returned unlocked for informational purposes.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
@Suspendable
|
||||||
|
fun unconsumedCashStatesForSpending(services: ServiceHub,
|
||||||
|
amount: Amount<Currency>,
|
||||||
|
onlyFromIssuerParties: Set<AbstractParty> = emptySet(),
|
||||||
|
notary: Party? = null,
|
||||||
|
lockId: UUID,
|
||||||
|
withIssuerRefs: Set<OpaqueBytes> = emptySet()): List<StateAndRef<Cash.State>> {
|
||||||
|
|
||||||
|
val issuerKeysStr = onlyFromIssuerParties.fold("") { left, right -> left + "('${right.owningKey.toBase58String()}')," }.dropLast(1)
|
||||||
|
val issuerRefsStr = withIssuerRefs.fold("") { left, right -> left + "('${right.bytes.toHexString()}')," }.dropLast(1)
|
||||||
|
|
||||||
|
val stateAndRefs = mutableListOf<StateAndRef<Cash.State>>()
|
||||||
|
|
||||||
|
// TODO: Need to provide a database provider independent means of performing this function.
|
||||||
|
// We are using an H2 specific means of selecting a minimum set of rows that match a request amount of coins:
|
||||||
|
// 1) There is no standard SQL mechanism of calculating a cumulative total on a field and restricting row selection on the
|
||||||
|
// running total of such an accumulator
|
||||||
|
// 2) H2 uses session variables to perform this accumulator function:
|
||||||
|
// http://www.h2database.com/html/functions.html#set
|
||||||
|
// 3) H2 does not support JOIN's in FOR UPDATE (hence we are forced to execute 2 queries)
|
||||||
|
|
||||||
|
for (retryCount in 1..MAX_RETRIES) {
|
||||||
|
|
||||||
|
spendLock.withLock {
|
||||||
|
val statement = services.jdbcSession().createStatement()
|
||||||
|
try {
|
||||||
|
statement.execute("CALL SET(@t, 0);")
|
||||||
|
|
||||||
|
// we select spendable states irrespective of lock but prioritised by unlocked ones (Eg. null)
|
||||||
|
// the softLockReserve update will detect whether we try to lock states locked by others
|
||||||
|
val selectJoin = """
|
||||||
|
SELECT vs.transaction_id, vs.output_index, vs.contract_state, ccs.pennies, SET(@t, ifnull(@t,0)+ccs.pennies) total_pennies, vs.lock_id
|
||||||
|
FROM vault_states AS vs, contract_cash_states AS ccs
|
||||||
|
WHERE vs.transaction_id = ccs.transaction_id AND vs.output_index = ccs.output_index
|
||||||
|
AND vs.state_status = 0
|
||||||
|
AND ccs.ccy_code = '${amount.token}' and @t < ${amount.quantity}
|
||||||
|
AND (vs.lock_id = '$lockId' OR vs.lock_id is null)
|
||||||
|
""" +
|
||||||
|
(if (notary != null)
|
||||||
|
" AND vs.notary_key = '${notary.owningKey.toBase58String()}'" else "") +
|
||||||
|
(if (onlyFromIssuerParties.isNotEmpty())
|
||||||
|
" AND ccs.issuer_key IN ($issuerKeysStr)" else "") +
|
||||||
|
(if (withIssuerRefs.isNotEmpty())
|
||||||
|
" AND ccs.issuer_ref IN ($issuerRefsStr)" else "")
|
||||||
|
|
||||||
|
// Retrieve spendable state refs
|
||||||
|
val rs = statement.executeQuery(selectJoin)
|
||||||
|
stateAndRefs.clear()
|
||||||
|
log.debug(selectJoin)
|
||||||
|
var totalPennies = 0L
|
||||||
|
while (rs.next()) {
|
||||||
|
val txHash = SecureHash.parse(rs.getString(1))
|
||||||
|
val index = rs.getInt(2)
|
||||||
|
val stateRef = StateRef(txHash, index)
|
||||||
|
val state = rs.getBytes(3).deserialize<TransactionState<Cash.State>>(context = SerializationDefaults.STORAGE_CONTEXT)
|
||||||
|
val pennies = rs.getLong(4)
|
||||||
|
totalPennies = rs.getLong(5)
|
||||||
|
val rowLockId = rs.getString(6)
|
||||||
|
stateAndRefs.add(StateAndRef(state, stateRef))
|
||||||
|
log.trace { "ROW: $rowLockId ($lockId): $stateRef : $pennies ($totalPennies)" }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateAndRefs.isNotEmpty() && totalPennies >= amount.quantity) {
|
||||||
|
// we should have a minimum number of states to satisfy our selection `amount` criteria
|
||||||
|
log.trace("Coin selection for $amount retrieved ${stateAndRefs.count()} states totalling $totalPennies pennies: $stateAndRefs")
|
||||||
|
|
||||||
|
// With the current single threaded state machine available states are guaranteed to lock.
|
||||||
|
// TODO However, we will have to revisit these methods in the future multi-threaded.
|
||||||
|
services.vaultService.softLockReserve(lockId, (stateAndRefs.map { it.ref }).toNonEmptySet())
|
||||||
|
return stateAndRefs
|
||||||
|
}
|
||||||
|
log.trace("Coin selection requested $amount but retrieved $totalPennies pennies with state refs: ${stateAndRefs.map { it.ref }}")
|
||||||
|
// retry as more states may become available
|
||||||
|
} catch (e: SQLException) {
|
||||||
|
log.error("""Failed retrieving unconsumed states for: amount [$amount], onlyFromIssuerParties [$onlyFromIssuerParties], notary [$notary], lockId [$lockId]
|
||||||
|
$e.
|
||||||
|
""")
|
||||||
|
} catch (e: StatesNotAvailableException) { // Should never happen with single threaded state machine
|
||||||
|
stateAndRefs.clear()
|
||||||
|
log.warn(e.message)
|
||||||
|
// retry only if there are locked states that may become available again (or consumed with change)
|
||||||
|
} finally {
|
||||||
|
statement.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.warn("Coin selection failed on attempt $retryCount")
|
||||||
|
// TODO: revisit the back off strategy for contended spending.
|
||||||
|
if (retryCount != MAX_RETRIES) {
|
||||||
|
Strand.sleep(RETRY_SLEEP * retryCount.toLong())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.warn("Insufficient spendable states identified for $amount")
|
||||||
|
return stateAndRefs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Small DSL extensions.
|
// Small DSL extensions.
|
||||||
|
@ -226,8 +226,6 @@ abstract class OnLedgerAsset<T : Any, C : CommandData, S : FungibleAsset<T>> : C
|
|||||||
*
|
*
|
||||||
* @param tx transaction builder to add states and commands to.
|
* @param tx transaction builder to add states and commands to.
|
||||||
* @param amountIssued the amount to be exited, represented as a quantity of issued currency.
|
* @param amountIssued the amount to be exited, represented as a quantity of issued currency.
|
||||||
* @param changeKey the key to send any change to. This needs to be explicitly stated as the input states are not
|
|
||||||
* necessarily owned by us.
|
|
||||||
* @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is
|
* @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is
|
||||||
* the responsibility of the caller to check that they do not exit funds held by others.
|
* the responsibility of the caller to check that they do not exit funds held by others.
|
||||||
* @return the public keys which must sign the transaction for it to be valid.
|
* @return the public keys which must sign the transaction for it to be valid.
|
||||||
|
@ -11,8 +11,8 @@ import net.corda.core.node.services.queryBy
|
|||||||
import net.corda.core.node.services.vault.DEFAULT_PAGE_NUM
|
import net.corda.core.node.services.vault.DEFAULT_PAGE_NUM
|
||||||
import net.corda.core.node.services.vault.PageSpecification
|
import net.corda.core.node.services.vault.PageSpecification
|
||||||
import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria
|
import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria
|
||||||
import net.corda.core.utilities.OpaqueBytes
|
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
|
import net.corda.core.utilities.OpaqueBytes
|
||||||
import net.corda.core.utilities.ProgressTracker
|
import net.corda.core.utilities.ProgressTracker
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@ -41,7 +41,7 @@ class CashExitFlow(val amount: Amount<Currency>, val issueRef: OpaqueBytes, prog
|
|||||||
progressTracker.currentStep = GENERATING_TX
|
progressTracker.currentStep = GENERATING_TX
|
||||||
val builder: TransactionBuilder = TransactionBuilder(notary = null as Party?)
|
val builder: TransactionBuilder = TransactionBuilder(notary = null as Party?)
|
||||||
val issuer = serviceHub.myInfo.legalIdentity.ref(issueRef)
|
val issuer = serviceHub.myInfo.legalIdentity.ref(issueRef)
|
||||||
val exitStates = serviceHub.vaultService.unconsumedStatesForSpending<Cash.State>(amount, setOf(issuer.party), builder.notary, builder.lockId, setOf(issuer.reference))
|
val exitStates = Cash.unconsumedCashStatesForSpending(serviceHub, amount, setOf(issuer.party), builder.notary, builder.lockId, setOf(issuer.reference))
|
||||||
val signers = try {
|
val signers = try {
|
||||||
Cash().generateExit(
|
Cash().generateExit(
|
||||||
builder,
|
builder,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package net.corda.flows
|
package net.corda.flows
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
|
import net.corda.contracts.asset.Cash
|
||||||
import net.corda.core.contracts.Amount
|
import net.corda.core.contracts.Amount
|
||||||
import net.corda.core.contracts.InsufficientBalanceException
|
import net.corda.core.contracts.InsufficientBalanceException
|
||||||
import net.corda.core.flows.StartableByRPC
|
import net.corda.core.flows.StartableByRPC
|
||||||
@ -26,7 +27,7 @@ open class CashPaymentFlow(
|
|||||||
val recipient: Party,
|
val recipient: Party,
|
||||||
val anonymous: Boolean,
|
val anonymous: Boolean,
|
||||||
progressTracker: ProgressTracker,
|
progressTracker: ProgressTracker,
|
||||||
val issuerConstraint: Set<Party>? = null) : AbstractCashFlow<AbstractCashFlow.Result>(progressTracker) {
|
val issuerConstraint: Set<Party> = emptySet()) : AbstractCashFlow<AbstractCashFlow.Result>(progressTracker) {
|
||||||
/** A straightforward constructor that constructs spends using cash states of any issuer. */
|
/** A straightforward constructor that constructs spends using cash states of any issuer. */
|
||||||
constructor(amount: Amount<Currency>, recipient: Party) : this(amount, recipient, true, tracker())
|
constructor(amount: Amount<Currency>, recipient: Party) : this(amount, recipient, true, tracker())
|
||||||
/** A straightforward constructor that constructs spends using cash states of any issuer. */
|
/** A straightforward constructor that constructs spends using cash states of any issuer. */
|
||||||
@ -45,7 +46,7 @@ open class CashPaymentFlow(
|
|||||||
val builder: TransactionBuilder = TransactionBuilder(null as Party?)
|
val builder: TransactionBuilder = TransactionBuilder(null as Party?)
|
||||||
// TODO: Have some way of restricting this to states the caller controls
|
// TODO: Have some way of restricting this to states the caller controls
|
||||||
val (spendTX, keysForSigning) = try {
|
val (spendTX, keysForSigning) = try {
|
||||||
serviceHub.vaultService.generateSpend(
|
Cash.generateSpend(serviceHub,
|
||||||
builder,
|
builder,
|
||||||
amount,
|
amount,
|
||||||
anonymousRecipient,
|
anonymousRecipient,
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
package net.corda.flows
|
package net.corda.flows
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
|
import net.corda.contracts.asset.Cash
|
||||||
import net.corda.contracts.asset.sumCashBy
|
import net.corda.contracts.asset.sumCashBy
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.Amount
|
||||||
|
import net.corda.core.contracts.OwnableState
|
||||||
|
import net.corda.core.contracts.StateAndRef
|
||||||
|
import net.corda.core.contracts.withoutIssuer
|
||||||
import net.corda.core.flows.*
|
import net.corda.core.flows.*
|
||||||
import net.corda.core.identity.AbstractParty
|
import net.corda.core.identity.AbstractParty
|
||||||
import net.corda.core.identity.AnonymousParty
|
import net.corda.core.identity.AnonymousParty
|
||||||
@ -174,7 +178,7 @@ object TwoPartyTradeFlow {
|
|||||||
val ptx = TransactionBuilder(notary)
|
val ptx = TransactionBuilder(notary)
|
||||||
|
|
||||||
// Add input and output states for the movement of cash, by using the Cash contract to generate the states
|
// Add input and output states for the movement of cash, by using the Cash contract to generate the states
|
||||||
val (tx, cashSigningPubKeys) = serviceHub.vaultService.generateSpend(ptx, tradeRequest.price, tradeRequest.sellerOwner)
|
val (tx, cashSigningPubKeys) = Cash.generateSpend(serviceHub, ptx, tradeRequest.price, tradeRequest.sellerOwner)
|
||||||
|
|
||||||
// Add inputs/outputs/a command for the movement of the asset.
|
// Add inputs/outputs/a command for the movement of the asset.
|
||||||
tx.addInputState(assetForSale)
|
tx.addInputState(assetForSale)
|
||||||
|
@ -1,21 +1,19 @@
|
|||||||
package net.corda.contracts
|
package net.corda.contracts
|
||||||
|
|
||||||
import net.corda.contracts.asset.*
|
import net.corda.contracts.asset.*
|
||||||
import net.corda.testing.contracts.fillWithSomeTestCash
|
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
import net.corda.core.utilities.days
|
|
||||||
import net.corda.core.identity.AnonymousParty
|
import net.corda.core.identity.AnonymousParty
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.node.services.Vault
|
import net.corda.core.node.services.Vault
|
||||||
import net.corda.core.node.services.VaultService
|
import net.corda.core.node.services.VaultService
|
||||||
import net.corda.core.utilities.seconds
|
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
import net.corda.node.utilities.configureDatabase
|
import net.corda.core.utilities.days
|
||||||
|
import net.corda.core.utilities.seconds
|
||||||
import net.corda.testing.*
|
import net.corda.testing.*
|
||||||
|
import net.corda.testing.contracts.fillWithSomeTestCash
|
||||||
import net.corda.testing.node.MockServices
|
import net.corda.testing.node.MockServices
|
||||||
import net.corda.testing.node.makeTestDataSourceProperties
|
import net.corda.testing.node.makeTestDatabaseAndMockServices
|
||||||
import net.corda.testing.node.makeTestDatabaseProperties
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.junit.runners.Parameterized
|
import org.junit.runners.Parameterized
|
||||||
@ -212,40 +210,22 @@ class CommercialPaperTestsGeneric {
|
|||||||
@Test
|
@Test
|
||||||
fun `issue move and then redeem`() {
|
fun `issue move and then redeem`() {
|
||||||
initialiseTestSerialization()
|
initialiseTestSerialization()
|
||||||
val dataSourcePropsAlice = makeTestDataSourceProperties()
|
val aliceDatabaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(ALICE_KEY))
|
||||||
val databaseAlice = configureDatabase(dataSourcePropsAlice, makeTestDatabaseProperties())
|
val databaseAlice = aliceDatabaseAndServices.first
|
||||||
|
aliceServices = aliceDatabaseAndServices.second
|
||||||
|
aliceVaultService = aliceServices.vaultService
|
||||||
|
|
||||||
databaseAlice.transaction {
|
databaseAlice.transaction {
|
||||||
|
|
||||||
aliceServices = object : MockServices(ALICE_KEY) {
|
|
||||||
override val vaultService: VaultService = makeVaultService(dataSourcePropsAlice)
|
|
||||||
|
|
||||||
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
|
|
||||||
for (stx in txs) {
|
|
||||||
validatedTransactions.addTransaction(stx)
|
|
||||||
}
|
|
||||||
// Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions.
|
|
||||||
vaultService.notifyAll(txs.map { it.tx })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
alicesVault = aliceServices.fillWithSomeTestCash(9000.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1)
|
alicesVault = aliceServices.fillWithSomeTestCash(9000.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1)
|
||||||
aliceVaultService = aliceServices.vaultService
|
aliceVaultService = aliceServices.vaultService
|
||||||
}
|
}
|
||||||
|
|
||||||
val dataSourcePropsBigCorp = makeTestDataSourceProperties()
|
val bigCorpDatabaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(BIG_CORP_KEY))
|
||||||
val databaseBigCorp = configureDatabase(dataSourcePropsBigCorp, makeTestDatabaseProperties())
|
val databaseBigCorp = bigCorpDatabaseAndServices.first
|
||||||
|
bigCorpServices = bigCorpDatabaseAndServices.second
|
||||||
|
bigCorpVaultService = bigCorpServices.vaultService
|
||||||
|
|
||||||
databaseBigCorp.transaction {
|
databaseBigCorp.transaction {
|
||||||
|
|
||||||
bigCorpServices = object : MockServices(BIG_CORP_KEY) {
|
|
||||||
override val vaultService: VaultService = makeVaultService(dataSourcePropsBigCorp)
|
|
||||||
|
|
||||||
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
|
|
||||||
for (stx in txs) {
|
|
||||||
validatedTransactions.addTransaction(stx)
|
|
||||||
}
|
|
||||||
// Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions.
|
|
||||||
vaultService.notifyAll(txs.map { it.tx })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
bigCorpVault = bigCorpServices.fillWithSomeTestCash(13000.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1)
|
bigCorpVault = bigCorpServices.fillWithSomeTestCash(13000.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1)
|
||||||
bigCorpVaultService = bigCorpServices.vaultService
|
bigCorpVaultService = bigCorpServices.vaultService
|
||||||
}
|
}
|
||||||
@ -266,7 +246,7 @@ class CommercialPaperTestsGeneric {
|
|||||||
// Alice pays $9000 to BigCorp to own some of their debt.
|
// Alice pays $9000 to BigCorp to own some of their debt.
|
||||||
moveTX = run {
|
moveTX = run {
|
||||||
val builder = TransactionBuilder(DUMMY_NOTARY)
|
val builder = TransactionBuilder(DUMMY_NOTARY)
|
||||||
aliceVaultService.generateSpend(builder, 9000.DOLLARS, AnonymousParty(bigCorpServices.key.public))
|
Cash.generateSpend(aliceServices, builder, 9000.DOLLARS, AnonymousParty(bigCorpServices.key.public))
|
||||||
CommercialPaper().generateMove(builder, issueTx.tx.outRef(0), AnonymousParty(aliceServices.key.public))
|
CommercialPaper().generateMove(builder, issueTx.tx.outRef(0), AnonymousParty(aliceServices.key.public))
|
||||||
val ptx = aliceServices.signInitialTransaction(builder)
|
val ptx = aliceServices.signInitialTransaction(builder)
|
||||||
val ptx2 = bigCorpServices.addSignature(ptx)
|
val ptx2 = bigCorpServices.addSignature(ptx)
|
||||||
@ -288,7 +268,7 @@ class CommercialPaperTestsGeneric {
|
|||||||
fun makeRedeemTX(time: Instant): Pair<SignedTransaction, UUID> {
|
fun makeRedeemTX(time: Instant): Pair<SignedTransaction, UUID> {
|
||||||
val builder = TransactionBuilder(DUMMY_NOTARY)
|
val builder = TransactionBuilder(DUMMY_NOTARY)
|
||||||
builder.setTimeWindow(time, 30.seconds)
|
builder.setTimeWindow(time, 30.seconds)
|
||||||
CommercialPaper().generateRedeem(builder, moveTX.tx.outRef(1), bigCorpVaultService)
|
CommercialPaper().generateRedeem(builder, moveTX.tx.outRef(1), bigCorpServices)
|
||||||
val ptx = aliceServices.signInitialTransaction(builder)
|
val ptx = aliceServices.signInitialTransaction(builder)
|
||||||
val ptx2 = bigCorpServices.addSignature(ptx)
|
val ptx2 = bigCorpServices.addSignature(ptx)
|
||||||
val stx = notaryServices.addSignature(ptx2)
|
val stx = notaryServices.addSignature(ptx2)
|
||||||
|
@ -6,26 +6,19 @@ import net.corda.core.crypto.generateKeyPair
|
|||||||
import net.corda.core.identity.AbstractParty
|
import net.corda.core.identity.AbstractParty
|
||||||
import net.corda.core.identity.AnonymousParty
|
import net.corda.core.identity.AnonymousParty
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.node.services.VaultQueryService
|
|
||||||
import net.corda.core.node.services.VaultService
|
import net.corda.core.node.services.VaultService
|
||||||
import net.corda.core.node.services.queryBy
|
import net.corda.core.node.services.queryBy
|
||||||
import net.corda.core.utilities.OpaqueBytes
|
|
||||||
import net.corda.core.transactions.SignedTransaction
|
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
import net.corda.core.transactions.WireTransaction
|
import net.corda.core.transactions.WireTransaction
|
||||||
import net.corda.node.services.database.HibernateConfiguration
|
import net.corda.core.utilities.OpaqueBytes
|
||||||
import net.corda.node.services.schema.NodeSchemaService
|
|
||||||
import net.corda.node.services.vault.HibernateVaultQueryImpl
|
|
||||||
import net.corda.node.services.vault.NodeVaultService
|
import net.corda.node.services.vault.NodeVaultService
|
||||||
import net.corda.node.utilities.CordaPersistence
|
import net.corda.node.utilities.CordaPersistence
|
||||||
import net.corda.node.utilities.configureDatabase
|
|
||||||
import net.corda.testing.*
|
import net.corda.testing.*
|
||||||
import net.corda.testing.contracts.DummyState
|
import net.corda.testing.contracts.DummyState
|
||||||
import net.corda.testing.contracts.fillWithSomeTestCash
|
import net.corda.testing.contracts.fillWithSomeTestCash
|
||||||
import net.corda.testing.node.MockKeyManagementService
|
|
||||||
import net.corda.testing.node.MockServices
|
import net.corda.testing.node.MockServices
|
||||||
import net.corda.testing.node.makeTestDataSourceProperties
|
import net.corda.testing.node.makeTestDatabaseAndMockServices
|
||||||
import net.corda.testing.node.makeTestDatabaseProperties
|
import org.junit.After
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.security.KeyPair
|
import java.security.KeyPair
|
||||||
@ -55,25 +48,11 @@ class CashTests : TestDependencyInjectionBase() {
|
|||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
LogHelper.setLevel(NodeVaultService::class)
|
LogHelper.setLevel(NodeVaultService::class)
|
||||||
val dataSourceProps = makeTestDataSourceProperties()
|
val databaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(MINI_CORP_KEY, MEGA_CORP_KEY, OUR_KEY))
|
||||||
database = configureDatabase(dataSourceProps, makeTestDatabaseProperties())
|
database = databaseAndServices.first
|
||||||
|
miniCorpServices = databaseAndServices.second
|
||||||
|
|
||||||
database.transaction {
|
database.transaction {
|
||||||
val hibernateConfig = HibernateConfiguration(NodeSchemaService(), makeTestDatabaseProperties())
|
|
||||||
miniCorpServices = object : MockServices(MINI_CORP_KEY) {
|
|
||||||
override val keyManagementService: MockKeyManagementService = MockKeyManagementService(identityService, MINI_CORP_KEY, MEGA_CORP_KEY, OUR_KEY)
|
|
||||||
override val vaultService: VaultService = makeVaultService(dataSourceProps)
|
|
||||||
|
|
||||||
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
|
|
||||||
for (stx in txs) {
|
|
||||||
validatedTransactions.addTransaction(stx)
|
|
||||||
}
|
|
||||||
// Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions.
|
|
||||||
vaultService.notifyAll(txs.map { it.tx })
|
|
||||||
}
|
|
||||||
|
|
||||||
override val vaultQueryService: VaultQueryService = HibernateVaultQueryImpl(hibernateConfig, vaultService.updatesPublisher)
|
|
||||||
}
|
|
||||||
|
|
||||||
miniCorpServices.fillWithSomeTestCash(howMuch = 100.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1,
|
miniCorpServices.fillWithSomeTestCash(howMuch = 100.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1,
|
||||||
issuedBy = MEGA_CORP.ref(1), issuerKey = MEGA_CORP_KEY, ownedBy = OUR_IDENTITY_1)
|
issuedBy = MEGA_CORP.ref(1), issuerKey = MEGA_CORP_KEY, ownedBy = OUR_IDENTITY_1)
|
||||||
miniCorpServices.fillWithSomeTestCash(howMuch = 400.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1,
|
miniCorpServices.fillWithSomeTestCash(howMuch = 400.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1,
|
||||||
@ -88,6 +67,11 @@ class CashTests : TestDependencyInjectionBase() {
|
|||||||
resetTestSerialization()
|
resetTestSerialization()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
database.close()
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun trivial() {
|
fun trivial() {
|
||||||
transaction {
|
transaction {
|
||||||
@ -485,7 +469,7 @@ class CashTests : TestDependencyInjectionBase() {
|
|||||||
fun makeSpend(amount: Amount<Currency>, dest: AbstractParty): WireTransaction {
|
fun makeSpend(amount: Amount<Currency>, dest: AbstractParty): WireTransaction {
|
||||||
val tx = TransactionBuilder(DUMMY_NOTARY)
|
val tx = TransactionBuilder(DUMMY_NOTARY)
|
||||||
database.transaction {
|
database.transaction {
|
||||||
vault.generateSpend(tx, amount, dest)
|
Cash.generateSpend(miniCorpServices, tx, amount, dest)
|
||||||
}
|
}
|
||||||
return tx.toWireTransaction()
|
return tx.toWireTransaction()
|
||||||
}
|
}
|
||||||
@ -586,7 +570,7 @@ class CashTests : TestDependencyInjectionBase() {
|
|||||||
database.transaction {
|
database.transaction {
|
||||||
|
|
||||||
val tx = TransactionBuilder(DUMMY_NOTARY)
|
val tx = TransactionBuilder(DUMMY_NOTARY)
|
||||||
vault.generateSpend(tx, 80.DOLLARS, ALICE, setOf(MINI_CORP))
|
Cash.generateSpend(miniCorpServices, tx, 80.DOLLARS, ALICE, setOf(MINI_CORP))
|
||||||
|
|
||||||
assertEquals(vaultStatesUnconsumed.elementAt(2).ref, tx.inputStates()[0])
|
assertEquals(vaultStatesUnconsumed.elementAt(2).ref, tx.inputStates()[0])
|
||||||
}
|
}
|
||||||
|
@ -87,7 +87,6 @@ processSmokeTestResources {
|
|||||||
// build/reports/project/dependencies/index.html for green highlighted parts of the tree.
|
// build/reports/project/dependencies/index.html for green highlighted parts of the tree.
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile project(':finance')
|
|
||||||
compile project(':node-schemas')
|
compile project(':node-schemas')
|
||||||
compile project(':node-api')
|
compile project(':node-api')
|
||||||
compile project(':client:rpc')
|
compile project(':client:rpc')
|
||||||
@ -150,6 +149,7 @@ dependencies {
|
|||||||
testCompile "com.pholser:junit-quickcheck-core:$quickcheck_version"
|
testCompile "com.pholser:junit-quickcheck-core:$quickcheck_version"
|
||||||
testCompile project(':test-utils')
|
testCompile project(':test-utils')
|
||||||
testCompile project(':client:jfx')
|
testCompile project(':client:jfx')
|
||||||
|
testCompile project(':finance')
|
||||||
|
|
||||||
// sample test schemas
|
// sample test schemas
|
||||||
testCompile project(path: ':finance', configuration: 'testArtifacts')
|
testCompile project(path: ':finance', configuration: 'testArtifacts')
|
||||||
|
@ -8,13 +8,13 @@ import net.corda.core.node.services.Vault
|
|||||||
import net.corda.core.node.services.VaultQueryException
|
import net.corda.core.node.services.VaultQueryException
|
||||||
import net.corda.core.node.services.vault.*
|
import net.corda.core.node.services.vault.*
|
||||||
import net.corda.core.node.services.vault.QueryCriteria.CommonQueryCriteria
|
import net.corda.core.node.services.vault.QueryCriteria.CommonQueryCriteria
|
||||||
|
import net.corda.core.schemas.CommonSchemaV1
|
||||||
import net.corda.core.schemas.PersistentState
|
import net.corda.core.schemas.PersistentState
|
||||||
import net.corda.core.schemas.PersistentStateRef
|
import net.corda.core.schemas.PersistentStateRef
|
||||||
import net.corda.core.utilities.OpaqueBytes
|
import net.corda.core.utilities.OpaqueBytes
|
||||||
import net.corda.core.utilities.toHexString
|
|
||||||
import net.corda.core.utilities.loggerFor
|
import net.corda.core.utilities.loggerFor
|
||||||
|
import net.corda.core.utilities.toHexString
|
||||||
import net.corda.core.utilities.trace
|
import net.corda.core.utilities.trace
|
||||||
import net.corda.core.schemas.CommonSchemaV1
|
|
||||||
import org.bouncycastle.asn1.x500.X500Name
|
import org.bouncycastle.asn1.x500.X500Name
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.persistence.Tuple
|
import javax.persistence.Tuple
|
||||||
|
@ -8,28 +8,32 @@ import io.requery.kotlin.eq
|
|||||||
import io.requery.kotlin.notNull
|
import io.requery.kotlin.notNull
|
||||||
import io.requery.query.RowExpression
|
import io.requery.query.RowExpression
|
||||||
import net.corda.contracts.asset.Cash
|
import net.corda.contracts.asset.Cash
|
||||||
import net.corda.contracts.asset.OnLedgerAsset
|
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.crypto.containsAny
|
import net.corda.core.crypto.containsAny
|
||||||
import net.corda.core.crypto.toBase58String
|
import net.corda.core.crypto.toBase58String
|
||||||
import net.corda.core.identity.AbstractParty
|
|
||||||
import net.corda.core.identity.Party
|
|
||||||
import net.corda.core.internal.ThreadBox
|
import net.corda.core.internal.ThreadBox
|
||||||
import net.corda.core.internal.tee
|
import net.corda.core.internal.tee
|
||||||
import net.corda.core.node.ServiceHub
|
import net.corda.core.node.ServiceHub
|
||||||
import net.corda.core.node.services.StatesNotAvailableException
|
import net.corda.core.node.services.StatesNotAvailableException
|
||||||
import net.corda.core.node.services.Vault
|
import net.corda.core.node.services.Vault
|
||||||
import net.corda.core.node.services.VaultService
|
import net.corda.core.node.services.VaultService
|
||||||
|
import net.corda.core.node.services.vault.IQueryCriteriaParser
|
||||||
|
import net.corda.core.node.services.vault.QueryCriteria
|
||||||
|
import net.corda.core.node.services.vault.Sort
|
||||||
|
import net.corda.core.node.services.vault.SortAttribute
|
||||||
|
import net.corda.core.schemas.PersistentState
|
||||||
import net.corda.core.serialization.SerializationDefaults.STORAGE_CONTEXT
|
import net.corda.core.serialization.SerializationDefaults.STORAGE_CONTEXT
|
||||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||||
import net.corda.core.serialization.deserialize
|
import net.corda.core.serialization.deserialize
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
import net.corda.core.transactions.CoreTransaction
|
import net.corda.core.transactions.CoreTransaction
|
||||||
import net.corda.core.transactions.NotaryChangeWireTransaction
|
import net.corda.core.transactions.NotaryChangeWireTransaction
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
|
||||||
import net.corda.core.transactions.WireTransaction
|
import net.corda.core.transactions.WireTransaction
|
||||||
import net.corda.core.utilities.*
|
import net.corda.core.utilities.NonEmptySet
|
||||||
|
import net.corda.core.utilities.loggerFor
|
||||||
|
import net.corda.core.utilities.toNonEmptySet
|
||||||
|
import net.corda.core.utilities.trace
|
||||||
import net.corda.node.services.database.RequeryConfiguration
|
import net.corda.node.services.database.RequeryConfiguration
|
||||||
import net.corda.node.services.database.parserTransactionIsolationLevel
|
import net.corda.node.services.database.parserTransactionIsolationLevel
|
||||||
import net.corda.node.services.statemachine.FlowStateMachineImpl
|
import net.corda.node.services.statemachine.FlowStateMachineImpl
|
||||||
@ -42,10 +46,8 @@ import net.corda.node.utilities.wrapWithDatabaseTransaction
|
|||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.subjects.PublishSubject
|
import rx.subjects.PublishSubject
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.sql.SQLException
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.locks.ReentrantLock
|
import javax.persistence.criteria.Predicate
|
||||||
import kotlin.concurrent.withLock
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Currently, the node vault service is a very simple RDBMS backed implementation. It will change significantly when
|
* Currently, the node vault service is a very simple RDBMS backed implementation. It will change significantly when
|
||||||
@ -79,6 +81,7 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P
|
|||||||
// For use during publishing only.
|
// For use during publishing only.
|
||||||
val updatesPublisher: rx.Observer<Vault.Update<ContractState>> get() = _updatesPublisher.bufferUntilDatabaseCommit().tee(_rawUpdatesPublisher)
|
val updatesPublisher: rx.Observer<Vault.Update<ContractState>> get() = _updatesPublisher.bufferUntilDatabaseCommit().tee(_rawUpdatesPublisher)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val mutex = ThreadBox(InnerState())
|
private val mutex = ThreadBox(InnerState())
|
||||||
|
|
||||||
private fun recordUpdate(update: Vault.Update<ContractState>): Vault.Update<ContractState> {
|
private fun recordUpdate(update: Vault.Update<ContractState>): Vault.Update<ContractState> {
|
||||||
@ -334,124 +337,111 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// coin selection retry loop counter, sleep (msecs) and lock for selecting states
|
// TODO We shouldn't need to rewrite the query if we could modify the defaults.
|
||||||
val MAX_RETRIES = 5
|
private class QueryEditor<out T : ContractState>(val services: ServiceHub,
|
||||||
val RETRY_SLEEP = 100
|
val lockId: UUID,
|
||||||
val spendLock: ReentrantLock = ReentrantLock()
|
val contractType: Class<out T>) : IQueryCriteriaParser {
|
||||||
|
var alreadyHasVaultQuery: Boolean = false
|
||||||
|
var modifiedCriteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(contractStateTypes = setOf(contractType),
|
||||||
|
softLockingCondition = QueryCriteria.SoftLockingCondition(QueryCriteria.SoftLockingType.UNLOCKED_AND_SPECIFIED, listOf(lockId)),
|
||||||
|
status = Vault.StateStatus.UNCONSUMED)
|
||||||
|
|
||||||
|
override fun parseCriteria(criteria: QueryCriteria.CommonQueryCriteria): Collection<Predicate> {
|
||||||
|
modifiedCriteria = criteria
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun parseCriteria(criteria: QueryCriteria.FungibleAssetQueryCriteria): Collection<Predicate> {
|
||||||
|
modifiedCriteria = criteria
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun parseCriteria(criteria: QueryCriteria.LinearStateQueryCriteria): Collection<Predicate> {
|
||||||
|
modifiedCriteria = criteria
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun <L : PersistentState> parseCriteria(criteria: QueryCriteria.VaultCustomQueryCriteria<L>): Collection<Predicate> {
|
||||||
|
modifiedCriteria = criteria
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun parseCriteria(criteria: QueryCriteria.VaultQueryCriteria): Collection<Predicate> {
|
||||||
|
modifiedCriteria = criteria.copy(contractStateTypes = setOf(contractType),
|
||||||
|
softLockingCondition = QueryCriteria.SoftLockingCondition(QueryCriteria.SoftLockingType.UNLOCKED_AND_SPECIFIED, listOf(lockId)),
|
||||||
|
status = Vault.StateStatus.UNCONSUMED)
|
||||||
|
alreadyHasVaultQuery = true
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun parseOr(left: QueryCriteria, right: QueryCriteria): Collection<Predicate> {
|
||||||
|
parse(left)
|
||||||
|
val modifiedLeft = modifiedCriteria
|
||||||
|
parse(right)
|
||||||
|
val modifiedRight = modifiedCriteria
|
||||||
|
modifiedCriteria = modifiedLeft.or(modifiedRight)
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun parseAnd(left: QueryCriteria, right: QueryCriteria): Collection<Predicate> {
|
||||||
|
parse(left)
|
||||||
|
val modifiedLeft = modifiedCriteria
|
||||||
|
parse(right)
|
||||||
|
val modifiedRight = modifiedCriteria
|
||||||
|
modifiedCriteria = modifiedLeft.and(modifiedRight)
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun parse(criteria: QueryCriteria, sorting: Sort?): Collection<Predicate> {
|
||||||
|
val basicQuery = modifiedCriteria
|
||||||
|
criteria.visit(this)
|
||||||
|
modifiedCriteria = if (alreadyHasVaultQuery) modifiedCriteria else criteria.and(basicQuery)
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun queryForEligibleStates(criteria: QueryCriteria): Vault.Page<T> {
|
||||||
|
val sortAttribute = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF)
|
||||||
|
val sorter = Sort(setOf(Sort.SortColumn(sortAttribute, Sort.Direction.ASC)))
|
||||||
|
parse(criteria, sorter)
|
||||||
|
|
||||||
|
return services.vaultQueryService.queryBy(contractType, modifiedCriteria, sorter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun <T : ContractState> unconsumedStatesForSpending(amount: Amount<Currency>, onlyFromIssuerParties: Set<AbstractParty>?, notary: Party?, lockId: UUID, withIssuerRefs: Set<OpaqueBytes>?): List<StateAndRef<T>> {
|
@Throws(StatesNotAvailableException::class)
|
||||||
|
override fun <T : FungibleAsset<U>, U : Any> tryLockFungibleStatesForSpending(lockId: UUID,
|
||||||
val issuerKeysStr = onlyFromIssuerParties?.fold("") { left, right -> left + "('${right.owningKey.toBase58String()}')," }?.dropLast(1)
|
eligibleStatesQuery: QueryCriteria,
|
||||||
val issuerRefsStr = withIssuerRefs?.fold("") { left, right -> left + "('${right.bytes.toHexString()}')," }?.dropLast(1)
|
amount: Amount<U>,
|
||||||
|
contractType: Class<out T>): List<StateAndRef<T>> {
|
||||||
val stateAndRefs = mutableListOf<StateAndRef<T>>()
|
if (amount.quantity == 0L) {
|
||||||
|
return emptyList()
|
||||||
// TODO: Need to provide a database provider independent means of performing this function.
|
|
||||||
// We are using an H2 specific means of selecting a minimum set of rows that match a request amount of coins:
|
|
||||||
// 1) There is no standard SQL mechanism of calculating a cumulative total on a field and restricting row selection on the
|
|
||||||
// running total of such an accumulator
|
|
||||||
// 2) H2 uses session variables to perform this accumulator function:
|
|
||||||
// http://www.h2database.com/html/functions.html#set
|
|
||||||
// 3) H2 does not support JOIN's in FOR UPDATE (hence we are forced to execute 2 queries)
|
|
||||||
|
|
||||||
for (retryCount in 1..MAX_RETRIES) {
|
|
||||||
|
|
||||||
spendLock.withLock {
|
|
||||||
val statement = configuration.jdbcSession().createStatement()
|
|
||||||
try {
|
|
||||||
statement.execute("CALL SET(@t, 0);")
|
|
||||||
|
|
||||||
// we select spendable states irrespective of lock but prioritised by unlocked ones (Eg. null)
|
|
||||||
// the softLockReserve update will detect whether we try to lock states locked by others
|
|
||||||
val selectJoin = """
|
|
||||||
SELECT vs.transaction_id, vs.output_index, vs.contract_state, ccs.pennies, SET(@t, ifnull(@t,0)+ccs.pennies) total_pennies, vs.lock_id
|
|
||||||
FROM vault_states AS vs, contract_cash_states AS ccs
|
|
||||||
WHERE vs.transaction_id = ccs.transaction_id AND vs.output_index = ccs.output_index
|
|
||||||
AND vs.state_status = 0
|
|
||||||
AND ccs.ccy_code = '${amount.token}' and @t < ${amount.quantity}
|
|
||||||
AND (vs.lock_id = '$lockId' OR vs.lock_id is null)
|
|
||||||
""" +
|
|
||||||
(if (notary != null)
|
|
||||||
" AND vs.notary_key = '${notary.owningKey.toBase58String()}'" else "") +
|
|
||||||
(if (issuerKeysStr != null)
|
|
||||||
" AND ccs.issuer_key IN ($issuerKeysStr)" else "") +
|
|
||||||
(if (issuerRefsStr != null)
|
|
||||||
" AND ccs.issuer_ref IN ($issuerRefsStr)" else "")
|
|
||||||
|
|
||||||
// Retrieve spendable state refs
|
|
||||||
val rs = statement.executeQuery(selectJoin)
|
|
||||||
stateAndRefs.clear()
|
|
||||||
log.debug(selectJoin)
|
|
||||||
var totalPennies = 0L
|
|
||||||
while (rs.next()) {
|
|
||||||
val txHash = SecureHash.parse(rs.getString(1))
|
|
||||||
val index = rs.getInt(2)
|
|
||||||
val stateRef = StateRef(txHash, index)
|
|
||||||
val state = rs.getBytes(3).deserialize<TransactionState<T>>(context = STORAGE_CONTEXT)
|
|
||||||
val pennies = rs.getLong(4)
|
|
||||||
totalPennies = rs.getLong(5)
|
|
||||||
val rowLockId = rs.getString(6)
|
|
||||||
stateAndRefs.add(StateAndRef(state, stateRef))
|
|
||||||
log.trace { "ROW: $rowLockId ($lockId): $stateRef : $pennies ($totalPennies)" }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stateAndRefs.isNotEmpty() && totalPennies >= amount.quantity) {
|
// TODO This helper code re-writes the query to alter the defaults on things such as soft locks
|
||||||
// we should have a minimum number of states to satisfy our selection `amount` criteria
|
// and then runs the query. Ideally we would not need to do this.
|
||||||
log.trace("Coin selection for $amount retrieved ${stateAndRefs.count()} states totalling $totalPennies pennies: $stateAndRefs")
|
val results = QueryEditor(services, lockId, contractType).queryForEligibleStates(eligibleStatesQuery)
|
||||||
|
|
||||||
// update database
|
var claimedAmount = 0L
|
||||||
softLockReserve(lockId, (stateAndRefs.map { it.ref }).toNonEmptySet())
|
val claimedStates = mutableListOf<StateAndRef<T>>()
|
||||||
return stateAndRefs
|
for (state in results.states) {
|
||||||
}
|
val issuedAssetToken = state.state.data.amount.token
|
||||||
log.trace("Coin selection requested $amount but retrieved $totalPennies pennies with state refs: ${stateAndRefs.map { it.ref }}")
|
if (issuedAssetToken.product == amount.token) {
|
||||||
// retry as more states may become available
|
claimedStates += state
|
||||||
} catch (e: SQLException) {
|
claimedAmount += state.state.data.amount.quantity
|
||||||
log.error("""Failed retrieving unconsumed states for: amount [$amount], onlyFromIssuerParties [$onlyFromIssuerParties], notary [$notary], lockId [$lockId]
|
if (claimedAmount > amount.quantity) {
|
||||||
$e.
|
break
|
||||||
""")
|
|
||||||
} catch (e: StatesNotAvailableException) {
|
|
||||||
stateAndRefs.clear()
|
|
||||||
log.warn(e.message)
|
|
||||||
// retry only if there are locked states that may become available again (or consumed with change)
|
|
||||||
} finally {
|
|
||||||
statement.close()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.warn("Coin selection failed on attempt $retryCount")
|
|
||||||
// TODO: revisit the back off strategy for contended spending.
|
|
||||||
if (retryCount != MAX_RETRIES) {
|
|
||||||
FlowStateMachineImpl.sleep(RETRY_SLEEP * retryCount.toLong())
|
|
||||||
}
|
}
|
||||||
|
if (claimedStates.isEmpty() || claimedAmount < amount.quantity) {
|
||||||
|
return emptyList()
|
||||||
}
|
}
|
||||||
|
softLockReserve(lockId, claimedStates.map { it.ref }.toNonEmptySet())
|
||||||
log.warn("Insufficient spendable states identified for $amount")
|
return claimedStates
|
||||||
return stateAndRefs
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a transaction that moves an amount of currency to the given pubkey.
|
|
||||||
*
|
|
||||||
* @param onlyFromParties if non-null, the asset states will be filtered to only include those issued by the set
|
|
||||||
* of given parties. This can be useful if the party you're trying to pay has expectations
|
|
||||||
* about which type of asset claims they are willing to accept.
|
|
||||||
*/
|
|
||||||
@Suspendable
|
|
||||||
override fun generateSpend(tx: TransactionBuilder,
|
|
||||||
amount: Amount<Currency>,
|
|
||||||
to: AbstractParty,
|
|
||||||
onlyFromParties: Set<AbstractParty>?): Pair<TransactionBuilder, List<PublicKey>> {
|
|
||||||
// Retrieve unspent and unlocked cash states that meet our spending criteria.
|
|
||||||
val acceptableCoins = unconsumedStatesForSpending<Cash.State>(amount, onlyFromParties, tx.notary, tx.lockId)
|
|
||||||
return OnLedgerAsset.generateSpend(tx, amount, to, acceptableCoins,
|
|
||||||
{ state, quantity, owner -> deriveState(state, quantity, owner) },
|
|
||||||
{ Cash().generateMoveCommand() })
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun deriveState(txState: TransactionState<Cash.State>, amount: Amount<Issued<Currency>>, owner: AbstractParty)
|
|
||||||
= txState.copy(data = txState.data.copy(amount = amount, owner = owner))
|
|
||||||
|
|
||||||
// TODO : Persists this in DB.
|
// TODO : Persists this in DB.
|
||||||
private val authorisedUpgrade = mutableMapOf<StateRef, Class<out UpgradedContract<*, *>>>()
|
private val authorisedUpgrade = mutableMapOf<StateRef, Class<out UpgradedContract<*, *>>>()
|
||||||
|
|
||||||
|
@ -1,42 +1,50 @@
|
|||||||
package net.corda.node.services.vault;
|
package net.corda.node.services.vault;
|
||||||
|
|
||||||
import com.google.common.collect.*;
|
import com.google.common.collect.ImmutableSet;
|
||||||
import net.corda.contracts.*;
|
import kotlin.Pair;
|
||||||
import net.corda.contracts.asset.*;
|
import net.corda.contracts.DealState;
|
||||||
|
import net.corda.contracts.asset.Cash;
|
||||||
import net.corda.core.contracts.*;
|
import net.corda.core.contracts.*;
|
||||||
import net.corda.core.crypto.*;
|
import net.corda.core.crypto.EncodingUtils;
|
||||||
import net.corda.core.identity.*;
|
import net.corda.core.identity.AbstractParty;
|
||||||
import net.corda.core.messaging.*;
|
import net.corda.core.messaging.DataFeed;
|
||||||
import net.corda.core.node.services.*;
|
import net.corda.core.node.services.Vault;
|
||||||
|
import net.corda.core.node.services.VaultQueryException;
|
||||||
|
import net.corda.core.node.services.VaultQueryService;
|
||||||
|
import net.corda.core.node.services.VaultService;
|
||||||
import net.corda.core.node.services.vault.*;
|
import net.corda.core.node.services.vault.*;
|
||||||
import net.corda.core.node.services.vault.QueryCriteria.*;
|
import net.corda.core.node.services.vault.QueryCriteria.LinearStateQueryCriteria;
|
||||||
import net.corda.core.schemas.*;
|
import net.corda.core.node.services.vault.QueryCriteria.VaultCustomQueryCriteria;
|
||||||
import net.corda.core.transactions.*;
|
import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria;
|
||||||
import net.corda.core.utilities.*;
|
import net.corda.core.utilities.OpaqueBytes;
|
||||||
import net.corda.node.services.database.*;
|
import net.corda.node.utilities.CordaPersistence;
|
||||||
import net.corda.node.services.schema.*;
|
import net.corda.schemas.CashSchemaV1;
|
||||||
import net.corda.node.utilities.*;
|
import net.corda.testing.TestConstants;
|
||||||
import net.corda.schemas.*;
|
import net.corda.testing.TestDependencyInjectionBase;
|
||||||
import net.corda.testing.*;
|
import net.corda.testing.contracts.DummyLinearContract;
|
||||||
import net.corda.testing.contracts.*;
|
import net.corda.testing.contracts.VaultFiller;
|
||||||
import net.corda.testing.node.*;
|
import net.corda.testing.node.MockServices;
|
||||||
import net.corda.testing.schemas.*;
|
import org.junit.After;
|
||||||
import org.jetbrains.annotations.*;
|
import org.junit.Before;
|
||||||
import org.junit.*;
|
import org.junit.Test;
|
||||||
import rx.Observable;
|
import rx.Observable;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.IOException;
|
||||||
import java.lang.reflect.*;
|
import java.lang.reflect.Field;
|
||||||
|
import java.security.KeyPair;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.*;
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import java.util.stream.StreamSupport;
|
||||||
|
|
||||||
import static net.corda.contracts.asset.CashKt.*;
|
import static net.corda.contracts.asset.CashKt.getDUMMY_CASH_ISSUER;
|
||||||
import static net.corda.core.node.services.vault.QueryCriteriaUtils.*;
|
import static net.corda.contracts.asset.CashKt.getDUMMY_CASH_ISSUER_KEY;
|
||||||
import static net.corda.core.utilities.ByteArrays.*;
|
import static net.corda.core.node.services.vault.QueryCriteriaUtils.DEFAULT_PAGE_NUM;
|
||||||
import static net.corda.node.utilities.CordaPersistenceKt.*;
|
import static net.corda.core.node.services.vault.QueryCriteriaUtils.MAX_PAGE_SIZE;
|
||||||
|
import static net.corda.core.utilities.ByteArrays.toHexString;
|
||||||
import static net.corda.testing.CoreTestUtils.*;
|
import static net.corda.testing.CoreTestUtils.*;
|
||||||
import static net.corda.testing.node.MockServicesKt.*;
|
import static net.corda.testing.node.MockServicesKt.makeTestDatabaseAndMockServices;
|
||||||
import static org.assertj.core.api.Assertions.*;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
public class VaultQueryJavaTests extends TestDependencyInjectionBase {
|
public class VaultQueryJavaTests extends TestDependencyInjectionBase {
|
||||||
|
|
||||||
@ -47,38 +55,13 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase {
|
|||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setUp() {
|
public void setUp() {
|
||||||
Properties dataSourceProps = makeTestDataSourceProperties(SecureHash.randomSHA256().toString());
|
ArrayList<KeyPair> keys = new ArrayList<>();
|
||||||
database = configureDatabase(dataSourceProps, makeTestDatabaseProperties());
|
keys.add(getMEGA_CORP_KEY());
|
||||||
database.transaction(statement -> {
|
Pair<CordaPersistence, MockServices> databaseAndServices = makeTestDatabaseAndMockServices(Collections.EMPTY_SET, keys);
|
||||||
Set<MappedSchema> customSchemas = new HashSet<>(Collections.singletonList(DummyLinearStateSchemaV1.INSTANCE));
|
database = databaseAndServices.getFirst();
|
||||||
HibernateConfiguration hibernateConfig = new HibernateConfiguration(new NodeSchemaService(customSchemas), makeTestDatabaseProperties());
|
services = databaseAndServices.getSecond();
|
||||||
services = new MockServices(getMEGA_CORP_KEY()) {
|
|
||||||
@NotNull
|
|
||||||
@Override
|
|
||||||
public VaultService getVaultService() {
|
|
||||||
if (vaultSvc != null) return vaultSvc;
|
|
||||||
return makeVaultService(dataSourceProps, hibernateConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
@Override
|
|
||||||
public VaultQueryService getVaultQueryService() {
|
|
||||||
return new HibernateVaultQueryImpl(hibernateConfig, vaultSvc.getUpdatesPublisher());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void recordTransactions(@NotNull Iterable<SignedTransaction> txs) {
|
|
||||||
for (SignedTransaction stx : txs) {
|
|
||||||
getValidatedTransactions().addTransaction(stx);
|
|
||||||
}
|
|
||||||
Stream<WireTransaction> wtxn = StreamSupport.stream(txs.spliterator(), false).map(SignedTransaction::getTx);
|
|
||||||
vaultSvc.notifyAll(wtxn.collect(Collectors.toList()));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
vaultSvc = services.getVaultService();
|
vaultSvc = services.getVaultService();
|
||||||
vaultQuerySvc = services.getVaultQueryService();
|
vaultQuerySvc = services.getVaultQueryService();
|
||||||
return services;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
|
@ -3,10 +3,12 @@ package net.corda.node.services.database
|
|||||||
import net.corda.contracts.asset.Cash
|
import net.corda.contracts.asset.Cash
|
||||||
import net.corda.contracts.asset.DUMMY_CASH_ISSUER
|
import net.corda.contracts.asset.DUMMY_CASH_ISSUER
|
||||||
import net.corda.contracts.asset.DummyFungibleContract
|
import net.corda.contracts.asset.DummyFungibleContract
|
||||||
|
import net.corda.contracts.asset.sumCash
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.crypto.toBase58String
|
import net.corda.core.crypto.toBase58String
|
||||||
import net.corda.core.node.services.Vault
|
import net.corda.core.node.services.Vault
|
||||||
|
import net.corda.core.node.services.VaultQueryService
|
||||||
import net.corda.core.node.services.VaultService
|
import net.corda.core.node.services.VaultService
|
||||||
import net.corda.core.schemas.CommonSchemaV1
|
import net.corda.core.schemas.CommonSchemaV1
|
||||||
import net.corda.core.schemas.PersistentStateRef
|
import net.corda.core.schemas.PersistentStateRef
|
||||||
@ -14,6 +16,7 @@ import net.corda.core.serialization.deserialize
|
|||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.node.services.schema.HibernateObserver
|
import net.corda.node.services.schema.HibernateObserver
|
||||||
import net.corda.node.services.schema.NodeSchemaService
|
import net.corda.node.services.schema.NodeSchemaService
|
||||||
|
import net.corda.node.services.vault.HibernateVaultQueryImpl
|
||||||
import net.corda.node.services.vault.NodeVaultService
|
import net.corda.node.services.vault.NodeVaultService
|
||||||
import net.corda.node.services.vault.VaultSchemaV1
|
import net.corda.node.services.vault.VaultSchemaV1
|
||||||
import net.corda.node.utilities.CordaPersistence
|
import net.corda.node.utilities.CordaPersistence
|
||||||
@ -38,6 +41,7 @@ import org.junit.After
|
|||||||
import org.junit.Assert
|
import org.junit.Assert
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import java.math.BigDecimal
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.persistence.EntityManager
|
import javax.persistence.EntityManager
|
||||||
@ -126,7 +130,8 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() {
|
|||||||
|
|
||||||
// execute query
|
// execute query
|
||||||
val queryResults = entityManager.createQuery(criteriaQuery).resultList
|
val queryResults = entityManager.createQuery(criteriaQuery).resultList
|
||||||
assertThat(queryResults.size).isEqualTo(6)
|
val coins = queryResults.map { it.contractState.deserialize<TransactionState<Cash.State>>().data }.sumCash()
|
||||||
|
assertThat(coins.toDecimal() >= BigDecimal("50.00"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -1,39 +1,40 @@
|
|||||||
package net.corda.node.services.vault
|
package net.corda.node.services.vault
|
||||||
|
|
||||||
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import net.corda.contracts.asset.Cash
|
import net.corda.contracts.asset.Cash
|
||||||
import net.corda.contracts.asset.DUMMY_CASH_ISSUER
|
import net.corda.contracts.asset.DUMMY_CASH_ISSUER
|
||||||
|
import net.corda.contracts.asset.sumCash
|
||||||
import net.corda.contracts.getCashBalance
|
import net.corda.contracts.getCashBalance
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
import net.corda.core.crypto.generateKeyPair
|
import net.corda.core.crypto.generateKeyPair
|
||||||
|
import net.corda.core.identity.AbstractParty
|
||||||
import net.corda.core.identity.AnonymousParty
|
import net.corda.core.identity.AnonymousParty
|
||||||
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.node.services.StatesNotAvailableException
|
import net.corda.core.node.services.StatesNotAvailableException
|
||||||
import net.corda.core.node.services.Vault
|
import net.corda.core.node.services.Vault
|
||||||
import net.corda.core.node.services.VaultQueryService
|
import net.corda.core.node.services.VaultQueryService
|
||||||
import net.corda.core.node.services.VaultService
|
import net.corda.core.node.services.VaultService
|
||||||
import net.corda.core.node.services.queryBy
|
import net.corda.core.node.services.queryBy
|
||||||
|
import net.corda.core.node.services.vault.QueryCriteria
|
||||||
import net.corda.core.node.services.vault.QueryCriteria.*
|
import net.corda.core.node.services.vault.QueryCriteria.*
|
||||||
import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria
|
|
||||||
import net.corda.core.transactions.NotaryChangeWireTransaction
|
import net.corda.core.transactions.NotaryChangeWireTransaction
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
import net.corda.core.utilities.NonEmptySet
|
import net.corda.core.utilities.NonEmptySet
|
||||||
import net.corda.core.utilities.OpaqueBytes
|
import net.corda.core.utilities.OpaqueBytes
|
||||||
import net.corda.core.utilities.toNonEmptySet
|
import net.corda.core.utilities.toNonEmptySet
|
||||||
import net.corda.node.services.database.HibernateConfiguration
|
|
||||||
import net.corda.node.services.schema.NodeSchemaService
|
|
||||||
import net.corda.node.utilities.CordaPersistence
|
import net.corda.node.utilities.CordaPersistence
|
||||||
import net.corda.node.utilities.configureDatabase
|
|
||||||
import net.corda.testing.*
|
import net.corda.testing.*
|
||||||
import net.corda.testing.contracts.fillWithSomeTestCash
|
import net.corda.testing.contracts.fillWithSomeTestCash
|
||||||
import net.corda.testing.node.MockServices
|
import net.corda.testing.node.MockServices
|
||||||
import net.corda.testing.node.makeTestDataSourceProperties
|
import net.corda.testing.node.makeTestDatabaseAndMockServices
|
||||||
import net.corda.testing.node.makeTestDatabaseProperties
|
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.assertj.core.api.Assertions.assertThatExceptionOfType
|
import org.assertj.core.api.Assertions.assertThatExceptionOfType
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import rx.observers.TestSubscriber
|
import rx.observers.TestSubscriber
|
||||||
|
import java.math.BigDecimal
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
@ -50,23 +51,9 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
|
|||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
LogHelper.setLevel(NodeVaultService::class)
|
LogHelper.setLevel(NodeVaultService::class)
|
||||||
val dataSourceProps = makeTestDataSourceProperties()
|
val databaseAndServices = makeTestDatabaseAndMockServices()
|
||||||
database = configureDatabase(dataSourceProps, makeTestDatabaseProperties())
|
database = databaseAndServices.first
|
||||||
database.transaction {
|
services = databaseAndServices.second
|
||||||
val hibernateConfig = HibernateConfiguration(NodeSchemaService(), makeTestDatabaseProperties())
|
|
||||||
services = object : MockServices() {
|
|
||||||
override val vaultService: VaultService = makeVaultService(dataSourceProps, hibernateConfig)
|
|
||||||
|
|
||||||
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
|
|
||||||
for (stx in txs) {
|
|
||||||
validatedTransactions.addTransaction(stx)
|
|
||||||
}
|
|
||||||
// Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions.
|
|
||||||
vaultService.notifyAll(txs.map { it.tx })
|
|
||||||
}
|
|
||||||
override val vaultQueryService : VaultQueryService = HibernateVaultQueryImpl(hibernateConfig, vaultService.updatesPublisher)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
@ -75,6 +62,25 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
|
|||||||
LogHelper.reset(NodeVaultService::class)
|
LogHelper.reset(NodeVaultService::class)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suspendable
|
||||||
|
private fun VaultService.unconsumedCashStatesForSpending(amount: Amount<Currency>,
|
||||||
|
onlyFromIssuerParties: Set<AbstractParty>? = null,
|
||||||
|
notary: Party? = null,
|
||||||
|
lockId: UUID = UUID.randomUUID(),
|
||||||
|
withIssuerRefs: Set<OpaqueBytes>? = null): List<StateAndRef<Cash.State>> {
|
||||||
|
|
||||||
|
val notaryName = if (notary != null) listOf(notary.name) else null
|
||||||
|
var baseCriteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(notaryName = notaryName)
|
||||||
|
if (onlyFromIssuerParties != null || withIssuerRefs != null) {
|
||||||
|
baseCriteria = baseCriteria.and(QueryCriteria.FungibleAssetQueryCriteria(
|
||||||
|
issuerPartyName = onlyFromIssuerParties?.toList(),
|
||||||
|
issuerRef = withIssuerRefs?.toList()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return tryLockFungibleStatesForSpending(lockId, baseCriteria, amount, Cash.State::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `states not local to instance`() {
|
fun `states not local to instance`() {
|
||||||
database.transaction {
|
database.transaction {
|
||||||
@ -308,7 +314,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
|
|||||||
val unconsumedStates = vaultQuery.queryBy<Cash.State>().states
|
val unconsumedStates = vaultQuery.queryBy<Cash.State>().states
|
||||||
assertThat(unconsumedStates).hasSize(1)
|
assertThat(unconsumedStates).hasSize(1)
|
||||||
|
|
||||||
val spendableStatesUSD = (vaultSvc as NodeVaultService).unconsumedStatesForSpending<Cash.State>(100.DOLLARS, lockId = UUID.randomUUID())
|
val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(100.DOLLARS)
|
||||||
spendableStatesUSD.forEach(::println)
|
spendableStatesUSD.forEach(::println)
|
||||||
assertThat(spendableStatesUSD).hasSize(1)
|
assertThat(spendableStatesUSD).hasSize(1)
|
||||||
assertThat(spendableStatesUSD[0].state.data.amount.quantity).isEqualTo(100L * 100)
|
assertThat(spendableStatesUSD[0].state.data.amount.quantity).isEqualTo(100L * 100)
|
||||||
@ -324,12 +330,13 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
|
|||||||
services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (DUMMY_CASH_ISSUER))
|
services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (DUMMY_CASH_ISSUER))
|
||||||
services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(1)), issuerKey = BOC_KEY)
|
services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(1)), issuerKey = BOC_KEY)
|
||||||
|
|
||||||
val spendableStatesUSD = vaultSvc.unconsumedStatesForSpending<Cash.State>(200.DOLLARS, lockId = UUID.randomUUID(),
|
val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(200.DOLLARS,
|
||||||
onlyFromIssuerParties = setOf(DUMMY_CASH_ISSUER.party, BOC)).toList()
|
onlyFromIssuerParties = setOf(DUMMY_CASH_ISSUER.party, BOC))
|
||||||
spendableStatesUSD.forEach(::println)
|
spendableStatesUSD.forEach(::println)
|
||||||
assertThat(spendableStatesUSD).hasSize(2)
|
assertThat(spendableStatesUSD).hasSize(2)
|
||||||
assertThat(spendableStatesUSD[0].state.data.amount.token.issuer).isEqualTo(DUMMY_CASH_ISSUER)
|
assertThat(spendableStatesUSD[0].state.data.amount.token.issuer).isIn(DUMMY_CASH_ISSUER, BOC.ref(1))
|
||||||
assertThat(spendableStatesUSD[1].state.data.amount.token.issuer).isEqualTo(BOC.ref(1))
|
assertThat(spendableStatesUSD[1].state.data.amount.token.issuer).isIn(DUMMY_CASH_ISSUER, BOC.ref(1))
|
||||||
|
assertThat(spendableStatesUSD[0].state.data.amount.token.issuer).isNotEqualTo(spendableStatesUSD[1].state.data.amount.token.issuer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -345,12 +352,13 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
|
|||||||
val unconsumedStates = vaultQuery.queryBy<Cash.State>().states
|
val unconsumedStates = vaultQuery.queryBy<Cash.State>().states
|
||||||
assertThat(unconsumedStates).hasSize(4)
|
assertThat(unconsumedStates).hasSize(4)
|
||||||
|
|
||||||
val spendableStatesUSD = vaultSvc.unconsumedStatesForSpending<Cash.State>(200.DOLLARS, lockId = UUID.randomUUID(),
|
val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(200.DOLLARS,
|
||||||
onlyFromIssuerParties = setOf(BOC), withIssuerRefs = setOf(OpaqueBytes.of(1), OpaqueBytes.of(2))).toList()
|
onlyFromIssuerParties = setOf(BOC), withIssuerRefs = setOf(OpaqueBytes.of(1), OpaqueBytes.of(2)))
|
||||||
assertThat(spendableStatesUSD).hasSize(2)
|
assertThat(spendableStatesUSD).hasSize(2)
|
||||||
assertThat(spendableStatesUSD[0].state.data.amount.token.issuer.party).isEqualTo(BOC)
|
assertThat(spendableStatesUSD[0].state.data.amount.token.issuer.party).isEqualTo(BOC)
|
||||||
assertThat(spendableStatesUSD[0].state.data.amount.token.issuer.reference).isEqualTo(BOC.ref(1).reference)
|
assertThat(spendableStatesUSD[0].state.data.amount.token.issuer.reference).isIn(BOC.ref(1).reference, BOC.ref(2).reference)
|
||||||
assertThat(spendableStatesUSD[1].state.data.amount.token.issuer.reference).isEqualTo(BOC.ref(2).reference)
|
assertThat(spendableStatesUSD[1].state.data.amount.token.issuer.reference).isIn(BOC.ref(1).reference, BOC.ref(2).reference)
|
||||||
|
assertThat(spendableStatesUSD[0].state.data.amount.token.issuer.reference).isNotEqualTo(spendableStatesUSD[1].state.data.amount.token.issuer.reference)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -363,9 +371,9 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
|
|||||||
val unconsumedStates = vaultQuery.queryBy<Cash.State>().states
|
val unconsumedStates = vaultQuery.queryBy<Cash.State>().states
|
||||||
assertThat(unconsumedStates).hasSize(1)
|
assertThat(unconsumedStates).hasSize(1)
|
||||||
|
|
||||||
val spendableStatesUSD = (vaultSvc as NodeVaultService).unconsumedStatesForSpending<Cash.State>(110.DOLLARS, lockId = UUID.randomUUID())
|
val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(110.DOLLARS)
|
||||||
spendableStatesUSD.forEach(::println)
|
spendableStatesUSD.forEach(::println)
|
||||||
assertThat(spendableStatesUSD).hasSize(1)
|
assertThat(spendableStatesUSD).hasSize(0)
|
||||||
val criteriaLocked = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.LOCKED_ONLY))
|
val criteriaLocked = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.LOCKED_ONLY))
|
||||||
assertThat(vaultQuery.queryBy<Cash.State>(criteriaLocked).states).hasSize(0)
|
assertThat(vaultQuery.queryBy<Cash.State>(criteriaLocked).states).hasSize(0)
|
||||||
}
|
}
|
||||||
@ -380,7 +388,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
|
|||||||
val unconsumedStates = vaultQuery.queryBy<Cash.State>().states
|
val unconsumedStates = vaultQuery.queryBy<Cash.State>().states
|
||||||
assertThat(unconsumedStates).hasSize(2)
|
assertThat(unconsumedStates).hasSize(2)
|
||||||
|
|
||||||
val spendableStatesUSD = (vaultSvc as NodeVaultService).unconsumedStatesForSpending<Cash.State>(1.DOLLARS, lockId = UUID.randomUUID())
|
val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(1.DOLLARS)
|
||||||
spendableStatesUSD.forEach(::println)
|
spendableStatesUSD.forEach(::println)
|
||||||
assertThat(spendableStatesUSD).hasSize(1)
|
assertThat(spendableStatesUSD).hasSize(1)
|
||||||
assertThat(spendableStatesUSD[0].state.data.amount.quantity).isGreaterThanOrEqualTo(100L)
|
assertThat(spendableStatesUSD[0].state.data.amount.quantity).isGreaterThanOrEqualTo(100L)
|
||||||
@ -397,16 +405,30 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
|
|||||||
services.fillWithSomeTestCash(100.POUNDS, DUMMY_NOTARY, 10, 10, Random(0L))
|
services.fillWithSomeTestCash(100.POUNDS, DUMMY_NOTARY, 10, 10, Random(0L))
|
||||||
services.fillWithSomeTestCash(100.SWISS_FRANCS, DUMMY_NOTARY, 10, 10, Random(0L))
|
services.fillWithSomeTestCash(100.SWISS_FRANCS, DUMMY_NOTARY, 10, 10, Random(0L))
|
||||||
|
|
||||||
|
var unlockedStates = 30
|
||||||
val allStates = vaultQuery.queryBy<Cash.State>().states
|
val allStates = vaultQuery.queryBy<Cash.State>().states
|
||||||
assertThat(allStates).hasSize(30)
|
assertThat(allStates).hasSize(unlockedStates)
|
||||||
|
|
||||||
|
var lockedCount = 0
|
||||||
for (i in 1..5) {
|
for (i in 1..5) {
|
||||||
val spendableStatesUSD = (vaultSvc as NodeVaultService).unconsumedStatesForSpending<Cash.State>(20.DOLLARS, lockId = UUID.randomUUID())
|
val lockId = UUID.randomUUID()
|
||||||
|
val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(20.DOLLARS, lockId = lockId)
|
||||||
spendableStatesUSD.forEach(::println)
|
spendableStatesUSD.forEach(::println)
|
||||||
|
assertThat(spendableStatesUSD.size <= unlockedStates)
|
||||||
|
unlockedStates -= spendableStatesUSD.size
|
||||||
|
val criteriaLocked = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.SPECIFIED, listOf(lockId)))
|
||||||
|
val lockedStates = vaultQuery.queryBy<Cash.State>(criteriaLocked).states
|
||||||
|
if (spendableStatesUSD.isNotEmpty()) {
|
||||||
|
assertEquals(spendableStatesUSD.size, lockedStates.size)
|
||||||
|
val lockedTotal = lockedStates.map { it.state.data }.sumCash()
|
||||||
|
val foundAmount = spendableStatesUSD.map { it.state.data }.sumCash()
|
||||||
|
assertThat(foundAmount.toDecimal() >= BigDecimal("20.00"))
|
||||||
|
assertThat(lockedTotal == foundAmount)
|
||||||
|
lockedCount += lockedStates.size
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// note only 3 spend attempts succeed with a total of 8 states
|
|
||||||
val criteriaLocked = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.LOCKED_ONLY))
|
val criteriaLocked = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.LOCKED_ONLY))
|
||||||
assertThat(vaultQuery.queryBy<Cash.State>(criteriaLocked).states).hasSize(8)
|
assertThat(vaultQuery.queryBy<Cash.State>(criteriaLocked).states).hasSize(lockedCount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -484,7 +506,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
|
|||||||
|
|
||||||
database.transaction {
|
database.transaction {
|
||||||
val moveTx = TransactionBuilder(services.myInfo.legalIdentity).apply {
|
val moveTx = TransactionBuilder(services.myInfo.legalIdentity).apply {
|
||||||
service.generateSpend(this, Amount(1000, GBP), thirdPartyIdentity)
|
Cash.generateSpend(services, this, Amount(1000, GBP), thirdPartyIdentity)
|
||||||
}.toWireTransaction()
|
}.toWireTransaction()
|
||||||
service.notify(moveTx)
|
service.notify(moveTx)
|
||||||
}
|
}
|
||||||
@ -530,7 +552,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
|
|||||||
// Move cash
|
// Move cash
|
||||||
val moveTx = database.transaction {
|
val moveTx = database.transaction {
|
||||||
TransactionBuilder(newNotary).apply {
|
TransactionBuilder(newNotary).apply {
|
||||||
service.generateSpend(this, Amount(1000, GBP), thirdPartyIdentity)
|
Cash.generateSpend(services, this, Amount(1000, GBP), thirdPartyIdentity)
|
||||||
}.toWireTransaction()
|
}.toWireTransaction()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,23 +1,19 @@
|
|||||||
package net.corda.node.services.vault
|
package net.corda.node.services.vault
|
||||||
|
|
||||||
import net.corda.contracts.*
|
import net.corda.contracts.CommercialPaper
|
||||||
|
import net.corda.contracts.Commodity
|
||||||
|
import net.corda.contracts.DealState
|
||||||
import net.corda.contracts.asset.Cash
|
import net.corda.contracts.asset.Cash
|
||||||
import net.corda.contracts.asset.DUMMY_CASH_ISSUER
|
import net.corda.contracts.asset.DUMMY_CASH_ISSUER
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.crypto.entropyToKeyPair
|
import net.corda.core.crypto.entropyToKeyPair
|
||||||
import net.corda.core.crypto.toBase58String
|
import net.corda.core.crypto.toBase58String
|
||||||
import net.corda.core.utilities.days
|
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.node.services.*
|
import net.corda.core.node.services.*
|
||||||
import net.corda.core.node.services.vault.*
|
import net.corda.core.node.services.vault.*
|
||||||
import net.corda.core.node.services.vault.QueryCriteria.*
|
import net.corda.core.node.services.vault.QueryCriteria.*
|
||||||
import net.corda.core.utilities.seconds
|
import net.corda.core.utilities.*
|
||||||
import net.corda.core.transactions.SignedTransaction
|
|
||||||
import net.corda.core.utilities.NonEmptySet
|
|
||||||
import net.corda.core.utilities.OpaqueBytes
|
|
||||||
import net.corda.core.utilities.toHexString
|
|
||||||
import net.corda.node.services.database.HibernateConfiguration
|
|
||||||
import net.corda.node.services.schema.NodeSchemaService
|
|
||||||
import net.corda.node.utilities.CordaPersistence
|
import net.corda.node.utilities.CordaPersistence
|
||||||
import net.corda.node.utilities.configureDatabase
|
import net.corda.node.utilities.configureDatabase
|
||||||
import net.corda.schemas.CashSchemaV1
|
import net.corda.schemas.CashSchemaV1
|
||||||
@ -27,7 +23,7 @@ import net.corda.schemas.SampleCashSchemaV3
|
|||||||
import net.corda.testing.*
|
import net.corda.testing.*
|
||||||
import net.corda.testing.contracts.*
|
import net.corda.testing.contracts.*
|
||||||
import net.corda.testing.node.MockServices
|
import net.corda.testing.node.MockServices
|
||||||
import net.corda.testing.node.makeTestDataSourceProperties
|
import net.corda.testing.node.makeTestDatabaseAndMockServices
|
||||||
import net.corda.testing.node.makeTestDatabaseProperties
|
import net.corda.testing.node.makeTestDatabaseProperties
|
||||||
import net.corda.testing.schemas.DummyLinearStateSchemaV1
|
import net.corda.testing.schemas.DummyLinearStateSchemaV1
|
||||||
import org.assertj.core.api.Assertions
|
import org.assertj.core.api.Assertions
|
||||||
@ -54,24 +50,9 @@ class VaultQueryTests : TestDependencyInjectionBase() {
|
|||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
val dataSourceProps = makeTestDataSourceProperties()
|
val databaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(MEGA_CORP_KEY))
|
||||||
database = configureDatabase(dataSourceProps, makeTestDatabaseProperties())
|
database = databaseAndServices.first
|
||||||
database.transaction {
|
services = databaseAndServices.second
|
||||||
val customSchemas = setOf(CommercialPaperSchemaV1, DummyLinearStateSchemaV1)
|
|
||||||
val hibernateConfig = HibernateConfiguration(NodeSchemaService(customSchemas), makeTestDatabaseProperties())
|
|
||||||
services = object : MockServices(MEGA_CORP_KEY) {
|
|
||||||
override val vaultService: VaultService = makeVaultService(dataSourceProps, hibernateConfig)
|
|
||||||
|
|
||||||
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
|
|
||||||
for (stx in txs) {
|
|
||||||
validatedTransactions.addTransaction(stx)
|
|
||||||
}
|
|
||||||
// Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions.
|
|
||||||
vaultService.notifyAll(txs.map { it.tx })
|
|
||||||
}
|
|
||||||
override val vaultQueryService : VaultQueryService = HibernateVaultQueryImpl(hibernateConfig, vaultService.updatesPublisher)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
@ -97,7 +78,7 @@ class VaultQueryTests : TestDependencyInjectionBase() {
|
|||||||
_database.transaction {
|
_database.transaction {
|
||||||
|
|
||||||
// create new states
|
// create new states
|
||||||
services.fillWithSomeTestCash(100.DOLLARS, CASH_NOTARY, 10, 10, Random(0L))
|
services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 10, 10, Random(0L))
|
||||||
val linearStatesXYZ = services.fillWithSomeTestLinearStates(1, "XYZ")
|
val linearStatesXYZ = services.fillWithSomeTestLinearStates(1, "XYZ")
|
||||||
val linearStatesJKL = services.fillWithSomeTestLinearStates(2, "JKL")
|
val linearStatesJKL = services.fillWithSomeTestLinearStates(2, "JKL")
|
||||||
services.fillWithSomeTestLinearStates(3, "ABC")
|
services.fillWithSomeTestLinearStates(3, "ABC")
|
||||||
@ -239,15 +220,15 @@ class VaultQueryTests : TestDependencyInjectionBase() {
|
|||||||
fun `unconsumed cash states sorted by state ref`() {
|
fun `unconsumed cash states sorted by state ref`() {
|
||||||
database.transaction {
|
database.transaction {
|
||||||
|
|
||||||
var stateRefs : MutableList<StateRef> = mutableListOf()
|
val stateRefs: MutableList<StateRef> = mutableListOf()
|
||||||
|
|
||||||
val issuedStates = services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 10, 10, Random(0L))
|
val issuedStates = services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 10, 10, Random(0L))
|
||||||
val issuedStateRefs = issuedStates.states.map { it.ref }.toList()
|
val issuedStateRefs = issuedStates.states.map { it.ref }.toList()
|
||||||
stateRefs.addAll(issuedStateRefs)
|
stateRefs.addAll(issuedStateRefs)
|
||||||
|
|
||||||
val spentStates = services.consumeCash(25.DOLLARS)
|
val spentStates = services.consumeCash(25.DOLLARS)
|
||||||
var consumedStateRefs = spentStates.consumed.map { it.ref }.toList()
|
val consumedStateRefs = spentStates.consumed.map { it.ref }.toList()
|
||||||
var producedStateRefs = spentStates.produced.map { it.ref }.toList()
|
val producedStateRefs = spentStates.produced.map { it.ref }.toList()
|
||||||
stateRefs.addAll(consumedStateRefs.plus(producedStateRefs))
|
stateRefs.addAll(consumedStateRefs.plus(producedStateRefs))
|
||||||
|
|
||||||
val sortAttribute = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF)
|
val sortAttribute = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF)
|
||||||
@ -271,8 +252,9 @@ class VaultQueryTests : TestDependencyInjectionBase() {
|
|||||||
fun `unconsumed cash states sorted by state ref txnId and index`() {
|
fun `unconsumed cash states sorted by state ref txnId and index`() {
|
||||||
database.transaction {
|
database.transaction {
|
||||||
services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 10, 10, Random(0L))
|
services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 10, 10, Random(0L))
|
||||||
services.consumeCash(10.DOLLARS)
|
val consumed = mutableSetOf<SecureHash>()
|
||||||
services.consumeCash(10.DOLLARS)
|
services.consumeCash(10.DOLLARS).consumed.forEach { consumed += it.ref.txhash }
|
||||||
|
services.consumeCash(10.DOLLARS).consumed.forEach { consumed += it.ref.txhash }
|
||||||
|
|
||||||
val sortAttributeTxnId = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF_TXN_ID)
|
val sortAttributeTxnId = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF_TXN_ID)
|
||||||
val sortAttributeIndex = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF_INDEX)
|
val sortAttributeIndex = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF_INDEX)
|
||||||
@ -283,13 +265,11 @@ class VaultQueryTests : TestDependencyInjectionBase() {
|
|||||||
|
|
||||||
results.statesMetadata.forEach {
|
results.statesMetadata.forEach {
|
||||||
println(" ${it.ref}")
|
println(" ${it.ref}")
|
||||||
|
assertThat(it.status).isEqualTo(Vault.StateStatus.UNCONSUMED)
|
||||||
}
|
}
|
||||||
|
val sorted = results.states.sortedBy { it.ref.toString() }
|
||||||
// explicit sort order asc by txnId and then index:
|
assertThat(results.states).isEqualTo(sorted)
|
||||||
// order by
|
assertThat(results.states).allSatisfy { !consumed.contains(it.ref.txhash) }
|
||||||
// vaultschem1_.transaction_id asc,
|
|
||||||
// vaultschem1_.output_index asc
|
|
||||||
assertThat(results.states).hasSize(9) // -2 CONSUMED + 1 NEW UNCONSUMED (change)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -411,7 +391,7 @@ class VaultQueryTests : TestDependencyInjectionBase() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val CASH_NOTARY_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(20)) }
|
val CASH_NOTARY_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(21)) }
|
||||||
val CASH_NOTARY: Party get() = Party(X500Name("CN=Cash Notary Service,O=R3,OU=corda,L=Zurich,C=CH"), CASH_NOTARY_KEY.public)
|
val CASH_NOTARY: Party get() = Party(X500Name("CN=Cash Notary Service,O=R3,OU=corda,L=Zurich,C=CH"), CASH_NOTARY_KEY.public)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -870,7 +850,7 @@ class VaultQueryTests : TestDependencyInjectionBase() {
|
|||||||
fun `aggregate functions count by contract type and state status`() {
|
fun `aggregate functions count by contract type and state status`() {
|
||||||
database.transaction {
|
database.transaction {
|
||||||
// create new states
|
// create new states
|
||||||
services.fillWithSomeTestCash(100.DOLLARS, CASH_NOTARY, 10, 10, Random(0L))
|
services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 10, 10, Random(0L))
|
||||||
val linearStatesXYZ = services.fillWithSomeTestLinearStates(1, "XYZ")
|
val linearStatesXYZ = services.fillWithSomeTestLinearStates(1, "XYZ")
|
||||||
val linearStatesJKL = services.fillWithSomeTestLinearStates(2, "JKL")
|
val linearStatesJKL = services.fillWithSomeTestLinearStates(2, "JKL")
|
||||||
services.fillWithSomeTestLinearStates(3, "ABC")
|
services.fillWithSomeTestLinearStates(3, "ABC")
|
||||||
@ -896,14 +876,14 @@ class VaultQueryTests : TestDependencyInjectionBase() {
|
|||||||
services.consumeLinearStates(linearStatesXYZ.states.toList())
|
services.consumeLinearStates(linearStatesXYZ.states.toList())
|
||||||
services.consumeLinearStates(linearStatesJKL.states.toList())
|
services.consumeLinearStates(linearStatesJKL.states.toList())
|
||||||
services.consumeDeals(dealStates.states.filter { it.state.data.ref == "456" })
|
services.consumeDeals(dealStates.states.filter { it.state.data.ref == "456" })
|
||||||
services.consumeCash(50.DOLLARS)
|
val cashUpdates = services.consumeCash(50.DOLLARS)
|
||||||
|
|
||||||
// UNCONSUMED states (default)
|
// UNCONSUMED states (default)
|
||||||
|
|
||||||
// count fungible assets
|
// count fungible assets
|
||||||
val countCriteriaUnconsumed = QueryCriteria.VaultCustomQueryCriteria(count, Vault.StateStatus.UNCONSUMED)
|
val countCriteriaUnconsumed = QueryCriteria.VaultCustomQueryCriteria(count, Vault.StateStatus.UNCONSUMED)
|
||||||
val fungibleStateCountUnconsumed = vaultQuerySvc.queryBy<FungibleAsset<*>>(countCriteriaUnconsumed).otherResults.single() as Long
|
val fungibleStateCountUnconsumed = vaultQuerySvc.queryBy<FungibleAsset<*>>(countCriteriaUnconsumed).otherResults.single() as Long
|
||||||
assertThat(fungibleStateCountUnconsumed).isEqualTo(5L)
|
assertThat(fungibleStateCountUnconsumed.toInt()).isEqualTo(10 - cashUpdates.consumed.size + cashUpdates.produced.size)
|
||||||
|
|
||||||
// count linear states
|
// count linear states
|
||||||
val linearStateCountUnconsumed = vaultQuerySvc.queryBy<LinearState>(countCriteriaUnconsumed).otherResults.single() as Long
|
val linearStateCountUnconsumed = vaultQuerySvc.queryBy<LinearState>(countCriteriaUnconsumed).otherResults.single() as Long
|
||||||
@ -918,7 +898,7 @@ class VaultQueryTests : TestDependencyInjectionBase() {
|
|||||||
// count fungible assets
|
// count fungible assets
|
||||||
val countCriteriaConsumed = QueryCriteria.VaultCustomQueryCriteria(count, Vault.StateStatus.CONSUMED)
|
val countCriteriaConsumed = QueryCriteria.VaultCustomQueryCriteria(count, Vault.StateStatus.CONSUMED)
|
||||||
val fungibleStateCountConsumed = vaultQuerySvc.queryBy<FungibleAsset<*>>(countCriteriaConsumed).otherResults.single() as Long
|
val fungibleStateCountConsumed = vaultQuerySvc.queryBy<FungibleAsset<*>>(countCriteriaConsumed).otherResults.single() as Long
|
||||||
assertThat(fungibleStateCountConsumed).isEqualTo(6L)
|
assertThat(fungibleStateCountConsumed.toInt()).isEqualTo(cashUpdates.consumed.size)
|
||||||
|
|
||||||
// count linear states
|
// count linear states
|
||||||
val linearStateCountConsumed = vaultQuerySvc.queryBy<LinearState>(countCriteriaConsumed).otherResults.single() as Long
|
val linearStateCountConsumed = vaultQuerySvc.queryBy<LinearState>(countCriteriaConsumed).otherResults.single() as Long
|
||||||
@ -962,7 +942,7 @@ class VaultQueryTests : TestDependencyInjectionBase() {
|
|||||||
fun `states consumed after time`() {
|
fun `states consumed after time`() {
|
||||||
database.transaction {
|
database.transaction {
|
||||||
|
|
||||||
services.fillWithSomeTestCash(100.DOLLARS, CASH_NOTARY, 3, 3, Random(0L))
|
services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L))
|
||||||
services.fillWithSomeTestLinearStates(10)
|
services.fillWithSomeTestLinearStates(10)
|
||||||
services.fillWithSomeTestDeals(listOf("123", "456", "789"))
|
services.fillWithSomeTestDeals(listOf("123", "456", "789"))
|
||||||
|
|
||||||
|
@ -11,17 +11,12 @@ import net.corda.core.node.services.VaultService
|
|||||||
import net.corda.core.node.services.queryBy
|
import net.corda.core.node.services.queryBy
|
||||||
import net.corda.core.node.services.vault.QueryCriteria
|
import net.corda.core.node.services.vault.QueryCriteria
|
||||||
import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria
|
import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria
|
||||||
import net.corda.core.transactions.SignedTransaction
|
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
import net.corda.node.services.database.HibernateConfiguration
|
|
||||||
import net.corda.node.services.schema.NodeSchemaService
|
|
||||||
import net.corda.node.utilities.CordaPersistence
|
import net.corda.node.utilities.CordaPersistence
|
||||||
import net.corda.node.utilities.configureDatabase
|
|
||||||
import net.corda.testing.*
|
import net.corda.testing.*
|
||||||
import net.corda.testing.contracts.*
|
import net.corda.testing.contracts.*
|
||||||
import net.corda.testing.node.MockServices
|
import net.corda.testing.node.MockServices
|
||||||
import net.corda.testing.node.makeTestDataSourceProperties
|
import net.corda.testing.node.makeTestDatabaseAndMockServices
|
||||||
import net.corda.testing.node.makeTestDatabaseProperties
|
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
@ -44,23 +39,9 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
|
|||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
LogHelper.setLevel(VaultWithCashTest::class)
|
LogHelper.setLevel(VaultWithCashTest::class)
|
||||||
val dataSourceProps = makeTestDataSourceProperties()
|
val databaseAndServices = makeTestDatabaseAndMockServices()
|
||||||
database = configureDatabase(dataSourceProps, makeTestDatabaseProperties())
|
database = databaseAndServices.first
|
||||||
database.transaction {
|
services = databaseAndServices.second
|
||||||
val hibernateConfig = HibernateConfiguration(NodeSchemaService(), makeTestDatabaseProperties())
|
|
||||||
services = object : MockServices() {
|
|
||||||
override val vaultService: VaultService = makeVaultService(dataSourceProps, hibernateConfig)
|
|
||||||
|
|
||||||
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
|
|
||||||
for (stx in txs) {
|
|
||||||
validatedTransactions.addTransaction(stx)
|
|
||||||
}
|
|
||||||
// Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions.
|
|
||||||
vaultService.notifyAll(txs.map { it.tx })
|
|
||||||
}
|
|
||||||
override val vaultQueryService : VaultQueryService = HibernateVaultQueryImpl(hibernateConfig, vaultService.updatesPublisher)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
@ -103,7 +84,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
|
|||||||
|
|
||||||
// A tx that spends our money.
|
// A tx that spends our money.
|
||||||
val spendTXBuilder = TransactionBuilder(DUMMY_NOTARY)
|
val spendTXBuilder = TransactionBuilder(DUMMY_NOTARY)
|
||||||
vault.generateSpend(spendTXBuilder, 80.DOLLARS, BOB)
|
Cash.generateSpend(services, spendTXBuilder, 80.DOLLARS, BOB)
|
||||||
val spendPTX = services.signInitialTransaction(spendTXBuilder, freshKey)
|
val spendPTX = services.signInitialTransaction(spendTXBuilder, freshKey)
|
||||||
val spendTX = notaryServices.addSignature(spendPTX)
|
val spendTX = notaryServices.addSignature(spendPTX)
|
||||||
|
|
||||||
@ -151,7 +132,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
|
|||||||
database.transaction {
|
database.transaction {
|
||||||
try {
|
try {
|
||||||
val txn1Builder = TransactionBuilder(DUMMY_NOTARY)
|
val txn1Builder = TransactionBuilder(DUMMY_NOTARY)
|
||||||
vault.generateSpend(txn1Builder, 60.DOLLARS, BOB)
|
Cash.generateSpend(services, txn1Builder, 60.DOLLARS, BOB)
|
||||||
val ptxn1 = notaryServices.signInitialTransaction(txn1Builder)
|
val ptxn1 = notaryServices.signInitialTransaction(txn1Builder)
|
||||||
val txn1 = services.addSignature(ptxn1, freshKey)
|
val txn1 = services.addSignature(ptxn1, freshKey)
|
||||||
println("txn1: ${txn1.id} spent ${((txn1.tx.outputs[0].data) as Cash.State).amount}")
|
println("txn1: ${txn1.id} spent ${((txn1.tx.outputs[0].data) as Cash.State).amount}")
|
||||||
@ -187,7 +168,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
|
|||||||
database.transaction {
|
database.transaction {
|
||||||
try {
|
try {
|
||||||
val txn2Builder = TransactionBuilder(DUMMY_NOTARY)
|
val txn2Builder = TransactionBuilder(DUMMY_NOTARY)
|
||||||
vault.generateSpend(txn2Builder, 80.DOLLARS, BOB)
|
Cash.generateSpend(services, txn2Builder, 80.DOLLARS, BOB)
|
||||||
val ptxn2 = notaryServices.signInitialTransaction(txn2Builder)
|
val ptxn2 = notaryServices.signInitialTransaction(txn2Builder)
|
||||||
val txn2 = services.addSignature(ptxn2, freshKey)
|
val txn2 = services.addSignature(ptxn2, freshKey)
|
||||||
println("txn2: ${txn2.id} spent ${((txn2.tx.outputs[0].data) as Cash.State).amount}")
|
println("txn2: ${txn2.id} spent ${((txn2.tx.outputs[0].data) as Cash.State).amount}")
|
||||||
@ -299,7 +280,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
|
|||||||
database.transaction {
|
database.transaction {
|
||||||
// A tx that spends our money.
|
// A tx that spends our money.
|
||||||
val spendTXBuilder = TransactionBuilder(DUMMY_NOTARY)
|
val spendTXBuilder = TransactionBuilder(DUMMY_NOTARY)
|
||||||
vault.generateSpend(spendTXBuilder, 80.DOLLARS, BOB)
|
Cash.generateSpend(services, spendTXBuilder, 80.DOLLARS, BOB)
|
||||||
val spendPTX = notaryServices.signInitialTransaction(spendTXBuilder)
|
val spendPTX = notaryServices.signInitialTransaction(spendTXBuilder)
|
||||||
val spendTX = services.addSignature(spendPTX, freshKey)
|
val spendTX = services.addSignature(spendPTX, freshKey)
|
||||||
services.recordTransactions(spendTX)
|
services.recordTransactions(spendTX)
|
||||||
|
@ -34,7 +34,7 @@ class DummyIssueAndMove(private val notary: Party, private val counterpartyNode:
|
|||||||
// Move ownership of the asset to the counterparty
|
// Move ownership of the asset to the counterparty
|
||||||
val moveTxBuilder = TransactionBuilder(notary = notary)
|
val moveTxBuilder = TransactionBuilder(notary = notary)
|
||||||
|
|
||||||
val (_, keys) = vaultService.generateSpend(moveTxBuilder, Amount(amount.quantity, GBP), counterpartyNode)
|
val (_, keys) = Cash.generateSpend(serviceHub, moveTxBuilder, Amount(amount.quantity, GBP), counterpartyNode)
|
||||||
// We don't check signatures because we know that the notary's signature is missing
|
// We don't check signatures because we know that the notary's signature is missing
|
||||||
signInitialTransaction(moveTxBuilder, keys)
|
signInitialTransaction(moveTxBuilder, keys)
|
||||||
}
|
}
|
||||||
|
@ -13,9 +13,9 @@ import net.corda.core.identity.Party
|
|||||||
import net.corda.core.node.ServiceHub
|
import net.corda.core.node.ServiceHub
|
||||||
import net.corda.core.node.services.Vault
|
import net.corda.core.node.services.Vault
|
||||||
import net.corda.core.toFuture
|
import net.corda.core.toFuture
|
||||||
import net.corda.core.utilities.OpaqueBytes
|
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
|
import net.corda.core.utilities.OpaqueBytes
|
||||||
import net.corda.testing.CHARLIE
|
import net.corda.testing.CHARLIE
|
||||||
import net.corda.testing.DUMMY_NOTARY
|
import net.corda.testing.DUMMY_NOTARY
|
||||||
import net.corda.testing.DUMMY_NOTARY_KEY
|
import net.corda.testing.DUMMY_NOTARY_KEY
|
||||||
@ -228,10 +228,11 @@ fun ServiceHub.evolveLinearState(linearState: StateAndRef<LinearState>) : StateA
|
|||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
fun ServiceHub.consumeCash(amount: Amount<Currency>, to: Party = CHARLIE): Vault.Update<ContractState> {
|
fun ServiceHub.consumeCash(amount: Amount<Currency>, to: Party = CHARLIE): Vault.Update<ContractState> {
|
||||||
val update = vaultService.rawUpdates.toFuture()
|
val update = vaultService.rawUpdates.toFuture()
|
||||||
|
val services = this
|
||||||
|
|
||||||
// A tx that spends our money.
|
// A tx that spends our money.
|
||||||
val spendTX = TransactionBuilder(DUMMY_NOTARY).apply {
|
val spendTX = TransactionBuilder(DUMMY_NOTARY).apply {
|
||||||
vaultService.generateSpend(this, amount, to)
|
Cash.generateSpend(services, this, amount, to)
|
||||||
signWith(DUMMY_NOTARY_KEY)
|
signWith(DUMMY_NOTARY_KEY)
|
||||||
}.toSignedTransaction(checkSufficientSignatures = false)
|
}.toSignedTransaction(checkSufficientSignatures = false)
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import net.corda.core.messaging.DataFeed
|
|||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.core.node.ServiceHub
|
import net.corda.core.node.ServiceHub
|
||||||
import net.corda.core.node.services.*
|
import net.corda.core.node.services.*
|
||||||
|
import net.corda.core.schemas.MappedSchema
|
||||||
import net.corda.core.serialization.SerializeAsToken
|
import net.corda.core.serialization.SerializeAsToken
|
||||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
@ -24,11 +25,14 @@ import net.corda.node.services.persistence.InMemoryStateMachineRecordedTransacti
|
|||||||
import net.corda.node.services.schema.HibernateObserver
|
import net.corda.node.services.schema.HibernateObserver
|
||||||
import net.corda.node.services.schema.NodeSchemaService
|
import net.corda.node.services.schema.NodeSchemaService
|
||||||
import net.corda.node.services.transactions.InMemoryTransactionVerifierService
|
import net.corda.node.services.transactions.InMemoryTransactionVerifierService
|
||||||
|
import net.corda.node.services.vault.HibernateVaultQueryImpl
|
||||||
import net.corda.node.services.vault.NodeVaultService
|
import net.corda.node.services.vault.NodeVaultService
|
||||||
import net.corda.testing.DUMMY_CA
|
import net.corda.node.utilities.CordaPersistence
|
||||||
import net.corda.testing.MEGA_CORP
|
import net.corda.node.utilities.configureDatabase
|
||||||
import net.corda.testing.MOCK_IDENTITIES
|
import net.corda.schemas.CashSchemaV1
|
||||||
import net.corda.testing.getTestPartyAndCertificate
|
import net.corda.schemas.CommercialPaperSchemaV1
|
||||||
|
import net.corda.testing.*
|
||||||
|
import net.corda.testing.schemas.DummyLinearStateSchemaV1
|
||||||
import org.bouncycastle.operator.ContentSigner
|
import org.bouncycastle.operator.ContentSigner
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.subjects.PublishSubject
|
import rx.subjects.PublishSubject
|
||||||
@ -212,4 +216,29 @@ fun makeTestDatabaseProperties(): Properties {
|
|||||||
return props
|
return props
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun makeTestDatabaseAndMockServices(customSchemas: Set<MappedSchema> = setOf(CommercialPaperSchemaV1, DummyLinearStateSchemaV1, CashSchemaV1), keys: List<KeyPair> = listOf(MEGA_CORP_KEY)): Pair<CordaPersistence, MockServices> {
|
||||||
|
val dataSourceProps = makeTestDataSourceProperties()
|
||||||
|
val databaseProperties = makeTestDatabaseProperties()
|
||||||
|
val database = configureDatabase(dataSourceProps, databaseProperties)
|
||||||
|
val mockService = database.transaction {
|
||||||
|
val hibernateConfig = HibernateConfiguration(NodeSchemaService(customSchemas), databaseProperties)
|
||||||
|
object : MockServices(*(keys.toTypedArray())) {
|
||||||
|
override val vaultService: VaultService = makeVaultService(dataSourceProps, hibernateConfig)
|
||||||
|
|
||||||
|
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
|
||||||
|
for (stx in txs) {
|
||||||
|
validatedTransactions.addTransaction(stx)
|
||||||
|
}
|
||||||
|
// Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions.
|
||||||
|
vaultService.notifyAll(txs.map { it.tx })
|
||||||
|
}
|
||||||
|
|
||||||
|
override val vaultQueryService: VaultQueryService = HibernateVaultQueryImpl(hibernateConfig, vaultService.updatesPublisher)
|
||||||
|
|
||||||
|
override fun jdbcSession(): Connection = database.createSession()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Pair(database, mockService)
|
||||||
|
}
|
||||||
|
|
||||||
val MOCK_VERSION_INFO = VersionInfo(1, "Mock release", "Mock revision", "Mock Vendor")
|
val MOCK_VERSION_INFO = VersionInfo(1, "Mock release", "Mock revision", "Mock Vendor")
|
||||||
|
Reference in New Issue
Block a user