mirror of
https://github.com/corda/corda.git
synced 2024-12-19 21:17:58 +00:00
Make a generic generate spend. Tests pass.
Fixup after rebase Fixup after rebase Make node have only test compile dependency against finance module. Use original query code. Fixup after rebase Update docs Edit docs Add to changelog Follow recommendations from PR Follow recommendations from PR Make a re-usable helper function to create MockServices with database for tests Tweak a few comments Don't include tryLockFungibleStateForSpending in soft lock docs. Respond to PR comments Fix whitespace error Fix compile error Fixup after rebase
This commit is contained in:
parent
9103b3b962
commit
f4f2d35375
@ -5,18 +5,13 @@ import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.messaging.DataFeed
|
||||
import net.corda.core.node.services.vault.QueryCriteria
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.toFuture
|
||||
import net.corda.core.transactions.CoreTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.NonEmptySet
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
@ -224,29 +219,7 @@ interface VaultService {
|
||||
|
||||
fun getTransactionNotes(txnId: SecureHash): Iterable<String>
|
||||
|
||||
/**
|
||||
* 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>>
|
||||
// DOCEND VaultStatesQuery
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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.
|
||||
*
|
||||
* @throws [StatesNotAvailableException] when not possible to softLock all of requested [StateRef]
|
||||
@ -273,21 +249,32 @@ interface VaultService {
|
||||
* are consumed as part of cash spending.
|
||||
*/
|
||||
fun softLockRelease(lockId: UUID, stateRefs: NonEmptySet<StateRef>? = null)
|
||||
|
||||
// DOCEND SoftLockAPI
|
||||
|
||||
/**
|
||||
* TODO: this function should be private to the vault, but currently Cash Exit functionality
|
||||
* is implemented in a separate module (finance) and requires access to it.
|
||||
* Helper function to combine using [VaultQueryService] calls to determine spendable states and soft locking them.
|
||||
* 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
|
||||
fun <T : ContractState> unconsumedStatesForSpending(amount: Amount<Currency>,
|
||||
onlyFromIssuerParties: Set<AbstractParty>? = null,
|
||||
notary: Party? = null,
|
||||
lockId: UUID,
|
||||
withIssuerRefs: Set<OpaqueBytes>? = null): List<StateAndRef<T>>
|
||||
@Throws(StatesNotAvailableException::class)
|
||||
fun <T : FungibleAsset<U>, U : Any> tryLockFungibleStatesForSpending(lockId: UUID,
|
||||
eligibleStatesQuery: QueryCriteria,
|
||||
amount: Amount<U>,
|
||||
contractType: Class<out T>): List<StateAndRef<T>>
|
||||
|
||||
}
|
||||
|
||||
|
||||
class StatesNotAvailableException(override val message: String?, override val cause: Throwable? = null) : FlowException(message, cause) {
|
||||
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
|
||||
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:
|
||||
|
||||
* 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.Issued
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.withoutIssuer
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
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.identity.Party
|
||||
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.builder
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
@ -31,9 +31,10 @@ private data class FxRequest(val tradeId: String,
|
||||
val notary: Party? = null)
|
||||
|
||||
// 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
|
||||
private fun gatherOurInputs(serviceHub: ServiceHub,
|
||||
lockId: UUID,
|
||||
amountRequired: Amount<Issued<Currency>>,
|
||||
notary: Party?): Pair<List<StateAndRef<Cash.State>>, Long> {
|
||||
// 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 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 cashCriteria = QueryCriteria.VaultCustomQueryCriteria(logicalExpression)
|
||||
|
||||
// Collect cash type inputs
|
||||
val suitableCashStates = serviceHub.vaultQueryService.queryBy<Cash.State>(fungibleCriteria.and(cashCriteria)).states
|
||||
require(!suitableCashStates.isEmpty()) { "Insufficient funds" }
|
||||
val fullCriteria = fungibleCriteria.and(vaultCriteria).and(cashCriteria)
|
||||
|
||||
var remaining = amountRequired.quantity
|
||||
// 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 eligibleStates = serviceHub.vaultService.tryLockFungibleStatesForSpending<Cash.State, Currency>(lockId, fullCriteria, amountRequired.withoutIssuer(), Cash.State::class.java)
|
||||
|
||||
val inputsList = mutableListOf<StateAndRef<Cash.State>>()
|
||||
// Iterate over filtered cash states to gather enough to pay
|
||||
for (cash in suitableCashStates.filter { it.state.notary == sourceNotary }) {
|
||||
inputsList += cash
|
||||
if (remaining <= cash.state.data.amount.quantity) {
|
||||
return Pair(inputsList, cash.state.data.amount.quantity - remaining)
|
||||
}
|
||||
remaining -= cash.state.data.amount.quantity
|
||||
}
|
||||
throw IllegalStateException("Insufficient funds")
|
||||
check(eligibleStates.isNotEmpty()) { "Insufficient funds" }
|
||||
val amount = eligibleStates.fold(0L) { tot, x -> tot + x.state.data.amount.quantity }
|
||||
val change = amount - amountRequired.quantity
|
||||
|
||||
return Pair(eligibleStates, change)
|
||||
}
|
||||
// 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
|
||||
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.
|
||||
// Putting this into a non-suspendable function also prevents issues when
|
||||
// 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
|
||||
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.")
|
||||
|
||||
// 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
|
||||
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.
|
||||
// Putting this into a non-suspendable function also prevent issues when
|
||||
// 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
|
||||
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
|
||||
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
|
||||
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
|
||||
|
@ -116,11 +116,12 @@ standard ``CashState`` in the ``:financial`` Gradle module. The Cash
|
||||
contract uses ``FungibleAsset`` states to model holdings of
|
||||
interchangeable assets and allow the split/merge and summing of
|
||||
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
|
||||
command. However, to elucidate more clearly example flow code is shown
|
||||
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
|
||||
:language: kotlin
|
||||
|
@ -680,9 +680,9 @@ Finally, we can do redemption.
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
@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.
|
||||
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.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
|
||||
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
|
||||
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
|
||||
|
@ -5,6 +5,7 @@ import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Iterables;
|
||||
import kotlin.Pair;
|
||||
import kotlin.Unit;
|
||||
import net.corda.contracts.asset.Cash;
|
||||
import net.corda.contracts.asset.CashKt;
|
||||
import net.corda.core.contracts.*;
|
||||
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.AnonymousParty;
|
||||
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.TransactionBuilder;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Currency;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static net.corda.core.contracts.ContractsDSL.requireSingleCommand;
|
||||
@ -255,8 +255,8 @@ public class JavaCommercialPaper implements Contract {
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
public void generateRedeem(TransactionBuilder tx, StateAndRef<State> paper, VaultService vault) throws InsufficientBalanceException {
|
||||
vault.generateSpend(tx, Structures.withoutIssuer(paper.getState().getData().getFaceValue()), paper.getState().getData().getOwner(), null);
|
||||
public void generateRedeem(TransactionBuilder tx, StateAndRef<State> paper, ServiceHub services) throws InsufficientBalanceException {
|
||||
Cash.generateSpend(services, tx, Structures.withoutIssuer(paper.getState().getData().getFaceValue()), paper.getState().getData().getOwner(), Collections.EMPTY_SET);
|
||||
tx.addInputState(paper);
|
||||
tx.addCommand(new Command<>(new Commands.Redeem(), paper.getState().getData().getOwner().getOwningKey()));
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.corda.contracts
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.contracts.asset.sumCashBy
|
||||
import net.corda.core.contracts.*
|
||||
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.Party
|
||||
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.PersistentState
|
||||
import net.corda.core.schemas.QueryableState
|
||||
@ -185,9 +186,9 @@ class CommercialPaper : Contract {
|
||||
*/
|
||||
@Throws(InsufficientBalanceException::class)
|
||||
@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.
|
||||
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.addCommand(Commands.Redeem(), paper.state.data.owner.owningKey)
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
package net.corda.contracts.asset
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import co.paralleluniverse.strands.Strand
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
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.Party
|
||||
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.PersistentState
|
||||
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.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 org.bouncycastle.asn1.x500.X500Name
|
||||
import java.math.BigInteger
|
||||
import java.security.PublicKey
|
||||
import java.sql.SQLException
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
|
@ -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 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
|
||||
* 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.
|
||||
|
@ -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.PageSpecification
|
||||
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.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import java.util.*
|
||||
|
||||
@ -41,7 +41,7 @@ class CashExitFlow(val amount: Amount<Currency>, val issueRef: OpaqueBytes, prog
|
||||
progressTracker.currentStep = GENERATING_TX
|
||||
val builder: TransactionBuilder = TransactionBuilder(notary = null as Party?)
|
||||
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 {
|
||||
Cash().generateExit(
|
||||
builder,
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.corda.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.InsufficientBalanceException
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
@ -26,7 +27,7 @@ open class CashPaymentFlow(
|
||||
val recipient: Party,
|
||||
val anonymous: Boolean,
|
||||
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. */
|
||||
constructor(amount: Amount<Currency>, recipient: Party) : this(amount, recipient, true, tracker())
|
||||
/** 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?)
|
||||
// TODO: Have some way of restricting this to states the caller controls
|
||||
val (spendTX, keysForSigning) = try {
|
||||
serviceHub.vaultService.generateSpend(
|
||||
Cash.generateSpend(serviceHub,
|
||||
builder,
|
||||
amount,
|
||||
anonymousRecipient,
|
||||
|
@ -1,8 +1,12 @@
|
||||
package net.corda.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.contracts.asset.Cash
|
||||
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.identity.AbstractParty
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
@ -174,7 +178,7 @@ object TwoPartyTradeFlow {
|
||||
val ptx = TransactionBuilder(notary)
|
||||
|
||||
// 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.
|
||||
tx.addInputState(assetForSale)
|
||||
|
@ -1,21 +1,19 @@
|
||||
package net.corda.contracts
|
||||
|
||||
import net.corda.contracts.asset.*
|
||||
import net.corda.testing.contracts.fillWithSomeTestCash
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.utilities.days
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.node.services.VaultService
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
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.contracts.fillWithSomeTestCash
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.node.makeTestDataSourceProperties
|
||||
import net.corda.testing.node.makeTestDatabaseProperties
|
||||
import net.corda.testing.node.makeTestDatabaseAndMockServices
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
@ -212,40 +210,22 @@ class CommercialPaperTestsGeneric {
|
||||
@Test
|
||||
fun `issue move and then redeem`() {
|
||||
initialiseTestSerialization()
|
||||
val dataSourcePropsAlice = makeTestDataSourceProperties()
|
||||
val databaseAlice = configureDatabase(dataSourcePropsAlice, makeTestDatabaseProperties())
|
||||
val aliceDatabaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(ALICE_KEY))
|
||||
val databaseAlice = aliceDatabaseAndServices.first
|
||||
aliceServices = aliceDatabaseAndServices.second
|
||||
aliceVaultService = aliceServices.vaultService
|
||||
|
||||
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)
|
||||
aliceVaultService = aliceServices.vaultService
|
||||
}
|
||||
|
||||
val dataSourcePropsBigCorp = makeTestDataSourceProperties()
|
||||
val databaseBigCorp = configureDatabase(dataSourcePropsBigCorp, makeTestDatabaseProperties())
|
||||
val bigCorpDatabaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(BIG_CORP_KEY))
|
||||
val databaseBigCorp = bigCorpDatabaseAndServices.first
|
||||
bigCorpServices = bigCorpDatabaseAndServices.second
|
||||
bigCorpVaultService = bigCorpServices.vaultService
|
||||
|
||||
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)
|
||||
bigCorpVaultService = bigCorpServices.vaultService
|
||||
}
|
||||
@ -266,7 +246,7 @@ class CommercialPaperTestsGeneric {
|
||||
// Alice pays $9000 to BigCorp to own some of their debt.
|
||||
moveTX = run {
|
||||
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))
|
||||
val ptx = aliceServices.signInitialTransaction(builder)
|
||||
val ptx2 = bigCorpServices.addSignature(ptx)
|
||||
@ -288,7 +268,7 @@ class CommercialPaperTestsGeneric {
|
||||
fun makeRedeemTX(time: Instant): Pair<SignedTransaction, UUID> {
|
||||
val builder = TransactionBuilder(DUMMY_NOTARY)
|
||||
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 ptx2 = bigCorpServices.addSignature(ptx)
|
||||
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.AnonymousParty
|
||||
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.queryBy
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.node.services.database.HibernateConfiguration
|
||||
import net.corda.node.services.schema.NodeSchemaService
|
||||
import net.corda.node.services.vault.HibernateVaultQueryImpl
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.node.services.vault.NodeVaultService
|
||||
import net.corda.node.utilities.CordaPersistence
|
||||
import net.corda.node.utilities.configureDatabase
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.contracts.DummyState
|
||||
import net.corda.testing.contracts.fillWithSomeTestCash
|
||||
import net.corda.testing.node.MockKeyManagementService
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.node.makeTestDataSourceProperties
|
||||
import net.corda.testing.node.makeTestDatabaseProperties
|
||||
import net.corda.testing.node.makeTestDatabaseAndMockServices
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.security.KeyPair
|
||||
@ -55,25 +48,11 @@ class CashTests : TestDependencyInjectionBase() {
|
||||
@Before
|
||||
fun setUp() {
|
||||
LogHelper.setLevel(NodeVaultService::class)
|
||||
val dataSourceProps = makeTestDataSourceProperties()
|
||||
database = configureDatabase(dataSourceProps, makeTestDatabaseProperties())
|
||||
val databaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(MINI_CORP_KEY, MEGA_CORP_KEY, OUR_KEY))
|
||||
database = databaseAndServices.first
|
||||
miniCorpServices = databaseAndServices.second
|
||||
|
||||
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,
|
||||
issuedBy = MEGA_CORP.ref(1), issuerKey = MEGA_CORP_KEY, ownedBy = OUR_IDENTITY_1)
|
||||
miniCorpServices.fillWithSomeTestCash(howMuch = 400.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1,
|
||||
@ -88,6 +67,11 @@ class CashTests : TestDependencyInjectionBase() {
|
||||
resetTestSerialization()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
database.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun trivial() {
|
||||
transaction {
|
||||
@ -485,7 +469,7 @@ class CashTests : TestDependencyInjectionBase() {
|
||||
fun makeSpend(amount: Amount<Currency>, dest: AbstractParty): WireTransaction {
|
||||
val tx = TransactionBuilder(DUMMY_NOTARY)
|
||||
database.transaction {
|
||||
vault.generateSpend(tx, amount, dest)
|
||||
Cash.generateSpend(miniCorpServices, tx, amount, dest)
|
||||
}
|
||||
return tx.toWireTransaction()
|
||||
}
|
||||
@ -586,7 +570,7 @@ class CashTests : TestDependencyInjectionBase() {
|
||||
database.transaction {
|
||||
|
||||
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])
|
||||
}
|
||||
|
@ -87,7 +87,6 @@ processSmokeTestResources {
|
||||
// build/reports/project/dependencies/index.html for green highlighted parts of the tree.
|
||||
|
||||
dependencies {
|
||||
compile project(':finance')
|
||||
compile project(':node-schemas')
|
||||
compile project(':node-api')
|
||||
compile project(':client:rpc')
|
||||
@ -150,6 +149,7 @@ dependencies {
|
||||
testCompile "com.pholser:junit-quickcheck-core:$quickcheck_version"
|
||||
testCompile project(':test-utils')
|
||||
testCompile project(':client:jfx')
|
||||
testCompile project(':finance')
|
||||
|
||||
// sample test schemas
|
||||
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.vault.*
|
||||
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.PersistentStateRef
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.toHexString
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.core.utilities.toHexString
|
||||
import net.corda.core.utilities.trace
|
||||
import net.corda.core.schemas.CommonSchemaV1
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import java.util.*
|
||||
import javax.persistence.Tuple
|
||||
|
@ -8,28 +8,32 @@ import io.requery.kotlin.eq
|
||||
import io.requery.kotlin.notNull
|
||||
import io.requery.query.RowExpression
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.contracts.asset.OnLedgerAsset
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.containsAny
|
||||
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.tee
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.services.StatesNotAvailableException
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.node.services.VaultService
|
||||
import net.corda.core.node.services.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.SingletonSerializeAsToken
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.CoreTransaction
|
||||
import net.corda.core.transactions.NotaryChangeWireTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
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.parserTransactionIsolationLevel
|
||||
import net.corda.node.services.statemachine.FlowStateMachineImpl
|
||||
@ -42,10 +46,8 @@ import net.corda.node.utilities.wrapWithDatabaseTransaction
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
import java.security.PublicKey
|
||||
import java.sql.SQLException
|
||||
import java.util.*
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.concurrent.withLock
|
||||
import javax.persistence.criteria.Predicate
|
||||
|
||||
/**
|
||||
* 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.
|
||||
val updatesPublisher: rx.Observer<Vault.Update<ContractState>> get() = _updatesPublisher.bufferUntilDatabaseCommit().tee(_rawUpdatesPublisher)
|
||||
}
|
||||
|
||||
private val mutex = ThreadBox(InnerState())
|
||||
|
||||
private fun recordUpdate(update: Vault.Update<ContractState>): Vault.Update<ContractState> {
|
||||
@ -334,123 +337,110 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P
|
||||
}
|
||||
}
|
||||
|
||||
// coin selection retry loop counter, sleep (msecs) and lock for selecting states
|
||||
val MAX_RETRIES = 5
|
||||
val RETRY_SLEEP = 100
|
||||
val spendLock: ReentrantLock = ReentrantLock()
|
||||
// TODO We shouldn't need to rewrite the query if we could modify the defaults.
|
||||
private class QueryEditor<out T : ContractState>(val services: ServiceHub,
|
||||
val lockId: UUID,
|
||||
val contractType: Class<out T>) : IQueryCriteriaParser {
|
||||
var alreadyHasVaultQuery: Boolean = false
|
||||
var modifiedCriteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(contractStateTypes = setOf(contractType),
|
||||
softLockingCondition = QueryCriteria.SoftLockingCondition(QueryCriteria.SoftLockingType.UNLOCKED_AND_SPECIFIED, listOf(lockId)),
|
||||
status = Vault.StateStatus.UNCONSUMED)
|
||||
|
||||
@Suspendable
|
||||
override fun <T : ContractState> unconsumedStatesForSpending(amount: Amount<Currency>, onlyFromIssuerParties: Set<AbstractParty>?, notary: Party?, lockId: UUID, withIssuerRefs: Set<OpaqueBytes>?): List<StateAndRef<T>> {
|
||||
|
||||
val issuerKeysStr = onlyFromIssuerParties?.fold("") { left, right -> left + "('${right.owningKey.toBase58String()}')," }?.dropLast(1)
|
||||
val issuerRefsStr = withIssuerRefs?.fold("") { left, right -> left + "('${right.bytes.toHexString()}')," }?.dropLast(1)
|
||||
|
||||
val stateAndRefs = mutableListOf<StateAndRef<T>>()
|
||||
|
||||
// TODO: Need to provide a database provider independent means of performing this function.
|
||||
// We are using an H2 specific means of selecting a minimum set of rows that match a request amount of coins:
|
||||
// 1) There is no standard SQL mechanism of calculating a cumulative total on a field and restricting row selection on the
|
||||
// running total of such an accumulator
|
||||
// 2) H2 uses session variables to perform this accumulator function:
|
||||
// http://www.h2database.com/html/functions.html#set
|
||||
// 3) H2 does not support JOIN's in FOR UPDATE (hence we are forced to execute 2 queries)
|
||||
|
||||
for (retryCount in 1..MAX_RETRIES) {
|
||||
|
||||
spendLock.withLock {
|
||||
val statement = configuration.jdbcSession().createStatement()
|
||||
try {
|
||||
statement.execute("CALL SET(@t, 0);")
|
||||
|
||||
// we select spendable states irrespective of lock but prioritised by unlocked ones (Eg. null)
|
||||
// the softLockReserve update will detect whether we try to lock states locked by others
|
||||
val selectJoin = """
|
||||
SELECT vs.transaction_id, vs.output_index, vs.contract_state, ccs.pennies, SET(@t, ifnull(@t,0)+ccs.pennies) total_pennies, vs.lock_id
|
||||
FROM vault_states AS vs, contract_cash_states AS ccs
|
||||
WHERE vs.transaction_id = ccs.transaction_id AND vs.output_index = ccs.output_index
|
||||
AND vs.state_status = 0
|
||||
AND ccs.ccy_code = '${amount.token}' and @t < ${amount.quantity}
|
||||
AND (vs.lock_id = '$lockId' OR vs.lock_id is null)
|
||||
""" +
|
||||
(if (notary != null)
|
||||
" AND vs.notary_key = '${notary.owningKey.toBase58String()}'" else "") +
|
||||
(if (issuerKeysStr != null)
|
||||
" AND ccs.issuer_key IN ($issuerKeysStr)" else "") +
|
||||
(if (issuerRefsStr != null)
|
||||
" AND ccs.issuer_ref IN ($issuerRefsStr)" else "")
|
||||
|
||||
// Retrieve spendable state refs
|
||||
val rs = statement.executeQuery(selectJoin)
|
||||
stateAndRefs.clear()
|
||||
log.debug(selectJoin)
|
||||
var totalPennies = 0L
|
||||
while (rs.next()) {
|
||||
val txHash = SecureHash.parse(rs.getString(1))
|
||||
val index = rs.getInt(2)
|
||||
val stateRef = StateRef(txHash, index)
|
||||
val state = rs.getBytes(3).deserialize<TransactionState<T>>(context = STORAGE_CONTEXT)
|
||||
val pennies = rs.getLong(4)
|
||||
totalPennies = rs.getLong(5)
|
||||
val rowLockId = rs.getString(6)
|
||||
stateAndRefs.add(StateAndRef(state, stateRef))
|
||||
log.trace { "ROW: $rowLockId ($lockId): $stateRef : $pennies ($totalPennies)" }
|
||||
}
|
||||
|
||||
if (stateAndRefs.isNotEmpty() && totalPennies >= amount.quantity) {
|
||||
// we should have a minimum number of states to satisfy our selection `amount` criteria
|
||||
log.trace("Coin selection for $amount retrieved ${stateAndRefs.count()} states totalling $totalPennies pennies: $stateAndRefs")
|
||||
|
||||
// update database
|
||||
softLockReserve(lockId, (stateAndRefs.map { it.ref }).toNonEmptySet())
|
||||
return stateAndRefs
|
||||
}
|
||||
log.trace("Coin selection requested $amount but retrieved $totalPennies pennies with state refs: ${stateAndRefs.map { it.ref }}")
|
||||
// retry as more states may become available
|
||||
} catch (e: SQLException) {
|
||||
log.error("""Failed retrieving unconsumed states for: amount [$amount], onlyFromIssuerParties [$onlyFromIssuerParties], notary [$notary], lockId [$lockId]
|
||||
$e.
|
||||
""")
|
||||
} catch (e: StatesNotAvailableException) {
|
||||
stateAndRefs.clear()
|
||||
log.warn(e.message)
|
||||
// retry only if there are locked states that may become available again (or consumed with change)
|
||||
} finally {
|
||||
statement.close()
|
||||
}
|
||||
}
|
||||
|
||||
log.warn("Coin selection failed on attempt $retryCount")
|
||||
// TODO: revisit the back off strategy for contended spending.
|
||||
if (retryCount != MAX_RETRIES) {
|
||||
FlowStateMachineImpl.sleep(RETRY_SLEEP * retryCount.toLong())
|
||||
}
|
||||
override fun parseCriteria(criteria: QueryCriteria.CommonQueryCriteria): Collection<Predicate> {
|
||||
modifiedCriteria = criteria
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
log.warn("Insufficient spendable states identified for $amount")
|
||||
return stateAndRefs
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() })
|
||||
}
|
||||
@Throws(StatesNotAvailableException::class)
|
||||
override fun <T : FungibleAsset<U>, U : Any> tryLockFungibleStatesForSpending(lockId: UUID,
|
||||
eligibleStatesQuery: QueryCriteria,
|
||||
amount: Amount<U>,
|
||||
contractType: Class<out T>): List<StateAndRef<T>> {
|
||||
if (amount.quantity == 0L) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
private fun deriveState(txState: TransactionState<Cash.State>, amount: Amount<Issued<Currency>>, owner: AbstractParty)
|
||||
= txState.copy(data = txState.data.copy(amount = amount, owner = owner))
|
||||
// TODO This helper code re-writes the query to alter the defaults on things such as soft locks
|
||||
// and then runs the query. Ideally we would not need to do this.
|
||||
val results = QueryEditor(services, lockId, contractType).queryForEligibleStates(eligibleStatesQuery)
|
||||
|
||||
var claimedAmount = 0L
|
||||
val claimedStates = mutableListOf<StateAndRef<T>>()
|
||||
for (state in results.states) {
|
||||
val issuedAssetToken = state.state.data.amount.token
|
||||
if (issuedAssetToken.product == amount.token) {
|
||||
claimedStates += state
|
||||
claimedAmount += state.state.data.amount.quantity
|
||||
if (claimedAmount > amount.quantity) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (claimedStates.isEmpty() || claimedAmount < amount.quantity) {
|
||||
return emptyList()
|
||||
}
|
||||
softLockReserve(lockId, claimedStates.map { it.ref }.toNonEmptySet())
|
||||
return claimedStates
|
||||
}
|
||||
|
||||
// TODO : Persists this in DB.
|
||||
private val authorisedUpgrade = mutableMapOf<StateRef, Class<out UpgradedContract<*, *>>>()
|
||||
|
@ -1,42 +1,50 @@
|
||||
package net.corda.node.services.vault;
|
||||
|
||||
import com.google.common.collect.*;
|
||||
import net.corda.contracts.*;
|
||||
import net.corda.contracts.asset.*;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import kotlin.Pair;
|
||||
import net.corda.contracts.DealState;
|
||||
import net.corda.contracts.asset.Cash;
|
||||
import net.corda.core.contracts.*;
|
||||
import net.corda.core.crypto.*;
|
||||
import net.corda.core.identity.*;
|
||||
import net.corda.core.messaging.*;
|
||||
import net.corda.core.node.services.*;
|
||||
import net.corda.core.crypto.EncodingUtils;
|
||||
import net.corda.core.identity.AbstractParty;
|
||||
import net.corda.core.messaging.DataFeed;
|
||||
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.QueryCriteria.*;
|
||||
import net.corda.core.schemas.*;
|
||||
import net.corda.core.transactions.*;
|
||||
import net.corda.core.utilities.*;
|
||||
import net.corda.node.services.database.*;
|
||||
import net.corda.node.services.schema.*;
|
||||
import net.corda.node.utilities.*;
|
||||
import net.corda.schemas.*;
|
||||
import net.corda.testing.*;
|
||||
import net.corda.testing.contracts.*;
|
||||
import net.corda.testing.node.*;
|
||||
import net.corda.testing.schemas.*;
|
||||
import org.jetbrains.annotations.*;
|
||||
import org.junit.*;
|
||||
import net.corda.core.node.services.vault.QueryCriteria.LinearStateQueryCriteria;
|
||||
import net.corda.core.node.services.vault.QueryCriteria.VaultCustomQueryCriteria;
|
||||
import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria;
|
||||
import net.corda.core.utilities.OpaqueBytes;
|
||||
import net.corda.node.utilities.CordaPersistence;
|
||||
import net.corda.schemas.CashSchemaV1;
|
||||
import net.corda.testing.TestConstants;
|
||||
import net.corda.testing.TestDependencyInjectionBase;
|
||||
import net.corda.testing.contracts.DummyLinearContract;
|
||||
import net.corda.testing.contracts.VaultFiller;
|
||||
import net.corda.testing.node.MockServices;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import rx.Observable;
|
||||
|
||||
import java.io.*;
|
||||
import java.lang.reflect.*;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.security.KeyPair;
|
||||
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.core.node.services.vault.QueryCriteriaUtils.*;
|
||||
import static net.corda.core.utilities.ByteArrays.*;
|
||||
import static net.corda.node.utilities.CordaPersistenceKt.*;
|
||||
import static net.corda.contracts.asset.CashKt.getDUMMY_CASH_ISSUER;
|
||||
import static net.corda.contracts.asset.CashKt.getDUMMY_CASH_ISSUER_KEY;
|
||||
import static net.corda.core.node.services.vault.QueryCriteriaUtils.DEFAULT_PAGE_NUM;
|
||||
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.node.MockServicesKt.*;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static net.corda.testing.node.MockServicesKt.makeTestDatabaseAndMockServices;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class VaultQueryJavaTests extends TestDependencyInjectionBase {
|
||||
|
||||
@ -47,38 +55,13 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase {
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
Properties dataSourceProps = makeTestDataSourceProperties(SecureHash.randomSHA256().toString());
|
||||
database = configureDatabase(dataSourceProps, makeTestDatabaseProperties());
|
||||
database.transaction(statement -> {
|
||||
Set<MappedSchema> customSchemas = new HashSet<>(Collections.singletonList(DummyLinearStateSchemaV1.INSTANCE));
|
||||
HibernateConfiguration hibernateConfig = new HibernateConfiguration(new NodeSchemaService(customSchemas), makeTestDatabaseProperties());
|
||||
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();
|
||||
vaultQuerySvc = services.getVaultQueryService();
|
||||
return services;
|
||||
});
|
||||
ArrayList<KeyPair> keys = new ArrayList<>();
|
||||
keys.add(getMEGA_CORP_KEY());
|
||||
Pair<CordaPersistence, MockServices> databaseAndServices = makeTestDatabaseAndMockServices(Collections.EMPTY_SET, keys);
|
||||
database = databaseAndServices.getFirst();
|
||||
services = databaseAndServices.getSecond();
|
||||
vaultSvc = services.getVaultService();
|
||||
vaultQuerySvc = services.getVaultQueryService();
|
||||
}
|
||||
|
||||
@After
|
||||
|
@ -148,8 +148,8 @@ class TwoPartyTradeFlowTests {
|
||||
bobNode.disableDBCloseOnStop()
|
||||
|
||||
val cashStates = bobNode.database.transaction {
|
||||
bobNode.services.fillWithSomeTestCash(2000.DOLLARS, notaryNode.info.notaryIdentity, 3, 3)
|
||||
}
|
||||
bobNode.services.fillWithSomeTestCash(2000.DOLLARS, notaryNode.info.notaryIdentity, 3, 3)
|
||||
}
|
||||
|
||||
val alicesFakePaper = aliceNode.database.transaction {
|
||||
fillUpForSeller(false, cpIssuer, aliceNode.info.legalIdentity,
|
||||
@ -168,7 +168,7 @@ class TwoPartyTradeFlowTests {
|
||||
}
|
||||
|
||||
val (bobStateMachine, aliceResult) = runBuyerAndSeller(notaryNode, aliceNode, bobNode,
|
||||
"alice's paper".outputStateAndRef())
|
||||
"alice's paper".outputStateAndRef())
|
||||
|
||||
assertEquals(aliceResult.getOrThrow(), bobStateMachine.getOrThrow().resultFuture.getOrThrow())
|
||||
|
||||
@ -533,11 +533,11 @@ class TwoPartyTradeFlowTests {
|
||||
override fun call(): SignedTransaction {
|
||||
send(buyer, Pair(notary.notaryIdentity, price))
|
||||
return subFlow(Seller(
|
||||
buyer,
|
||||
notary,
|
||||
assetToSell,
|
||||
price,
|
||||
me.party))
|
||||
buyer,
|
||||
notary,
|
||||
assetToSell,
|
||||
price,
|
||||
me.party))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,10 +3,12 @@ package net.corda.node.services.database
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.contracts.asset.DUMMY_CASH_ISSUER
|
||||
import net.corda.contracts.asset.DummyFungibleContract
|
||||
import net.corda.contracts.asset.sumCash
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.toBase58String
|
||||
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.schemas.CommonSchemaV1
|
||||
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.node.services.schema.HibernateObserver
|
||||
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.VaultSchemaV1
|
||||
import net.corda.node.utilities.CordaPersistence
|
||||
@ -38,6 +41,7 @@ import org.junit.After
|
||||
import org.junit.Assert
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.math.BigDecimal
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import javax.persistence.EntityManager
|
||||
@ -126,7 +130,8 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() {
|
||||
|
||||
// execute query
|
||||
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
|
||||
|
@ -1,39 +1,40 @@
|
||||
package net.corda.node.services.vault
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.contracts.asset.DUMMY_CASH_ISSUER
|
||||
import net.corda.contracts.asset.sumCash
|
||||
import net.corda.contracts.getCashBalance
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.identity.AbstractParty
|
||||
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.Vault
|
||||
import net.corda.core.node.services.VaultQueryService
|
||||
import net.corda.core.node.services.VaultService
|
||||
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.transactions.NotaryChangeWireTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.NonEmptySet
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
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.configureDatabase
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.contracts.fillWithSomeTestCash
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.node.makeTestDataSourceProperties
|
||||
import net.corda.testing.node.makeTestDatabaseProperties
|
||||
import net.corda.testing.node.makeTestDatabaseAndMockServices
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatExceptionOfType
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import rx.observers.TestSubscriber
|
||||
import java.math.BigDecimal
|
||||
import java.util.*
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.Executors
|
||||
@ -50,23 +51,9 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
|
||||
@Before
|
||||
fun setUp() {
|
||||
LogHelper.setLevel(NodeVaultService::class)
|
||||
val dataSourceProps = makeTestDataSourceProperties()
|
||||
database = configureDatabase(dataSourceProps, makeTestDatabaseProperties())
|
||||
database.transaction {
|
||||
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)
|
||||
}
|
||||
}
|
||||
val databaseAndServices = makeTestDatabaseAndMockServices()
|
||||
database = databaseAndServices.first
|
||||
services = databaseAndServices.second
|
||||
}
|
||||
|
||||
@After
|
||||
@ -75,6 +62,25 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
|
||||
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
|
||||
fun `states not local to instance`() {
|
||||
database.transaction {
|
||||
@ -308,7 +314,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
|
||||
val unconsumedStates = vaultQuery.queryBy<Cash.State>().states
|
||||
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)
|
||||
assertThat(spendableStatesUSD).hasSize(1)
|
||||
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 = (BOC.ref(1)), issuerKey = BOC_KEY)
|
||||
|
||||
val spendableStatesUSD = vaultSvc.unconsumedStatesForSpending<Cash.State>(200.DOLLARS, lockId = UUID.randomUUID(),
|
||||
onlyFromIssuerParties = setOf(DUMMY_CASH_ISSUER.party, BOC)).toList()
|
||||
val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(200.DOLLARS,
|
||||
onlyFromIssuerParties = setOf(DUMMY_CASH_ISSUER.party, BOC))
|
||||
spendableStatesUSD.forEach(::println)
|
||||
assertThat(spendableStatesUSD).hasSize(2)
|
||||
assertThat(spendableStatesUSD[0].state.data.amount.token.issuer).isEqualTo(DUMMY_CASH_ISSUER)
|
||||
assertThat(spendableStatesUSD[1].state.data.amount.token.issuer).isEqualTo(BOC.ref(1))
|
||||
assertThat(spendableStatesUSD[0].state.data.amount.token.issuer).isIn(DUMMY_CASH_ISSUER, 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
|
||||
assertThat(unconsumedStates).hasSize(4)
|
||||
|
||||
val spendableStatesUSD = vaultSvc.unconsumedStatesForSpending<Cash.State>(200.DOLLARS, lockId = UUID.randomUUID(),
|
||||
onlyFromIssuerParties = setOf(BOC), withIssuerRefs = setOf(OpaqueBytes.of(1), OpaqueBytes.of(2))).toList()
|
||||
val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(200.DOLLARS,
|
||||
onlyFromIssuerParties = setOf(BOC), withIssuerRefs = setOf(OpaqueBytes.of(1), OpaqueBytes.of(2)))
|
||||
assertThat(spendableStatesUSD).hasSize(2)
|
||||
assertThat(spendableStatesUSD[0].state.data.amount.token.issuer.party).isEqualTo(BOC)
|
||||
assertThat(spendableStatesUSD[0].state.data.amount.token.issuer.reference).isEqualTo(BOC.ref(1).reference)
|
||||
assertThat(spendableStatesUSD[1].state.data.amount.token.issuer.reference).isEqualTo(BOC.ref(2).reference)
|
||||
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).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
|
||||
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)
|
||||
assertThat(spendableStatesUSD).hasSize(1)
|
||||
assertThat(spendableStatesUSD).hasSize(0)
|
||||
val criteriaLocked = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.LOCKED_ONLY))
|
||||
assertThat(vaultQuery.queryBy<Cash.State>(criteriaLocked).states).hasSize(0)
|
||||
}
|
||||
@ -380,7 +388,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
|
||||
val unconsumedStates = vaultQuery.queryBy<Cash.State>().states
|
||||
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)
|
||||
assertThat(spendableStatesUSD).hasSize(1)
|
||||
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.SWISS_FRANCS, DUMMY_NOTARY, 10, 10, Random(0L))
|
||||
|
||||
var unlockedStates = 30
|
||||
val allStates = vaultQuery.queryBy<Cash.State>().states
|
||||
assertThat(allStates).hasSize(30)
|
||||
assertThat(allStates).hasSize(unlockedStates)
|
||||
|
||||
var lockedCount = 0
|
||||
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)
|
||||
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))
|
||||
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 {
|
||||
val moveTx = TransactionBuilder(services.myInfo.legalIdentity).apply {
|
||||
service.generateSpend(this, Amount(1000, GBP), thirdPartyIdentity)
|
||||
Cash.generateSpend(services, this, Amount(1000, GBP), thirdPartyIdentity)
|
||||
}.toWireTransaction()
|
||||
service.notify(moveTx)
|
||||
}
|
||||
@ -530,7 +552,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
|
||||
// Move cash
|
||||
val moveTx = database.transaction {
|
||||
TransactionBuilder(newNotary).apply {
|
||||
service.generateSpend(this, Amount(1000, GBP), thirdPartyIdentity)
|
||||
Cash.generateSpend(services, this, Amount(1000, GBP), thirdPartyIdentity)
|
||||
}.toWireTransaction()
|
||||
}
|
||||
|
||||
|
@ -1,23 +1,19 @@
|
||||
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.DUMMY_CASH_ISSUER
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.entropyToKeyPair
|
||||
import net.corda.core.crypto.toBase58String
|
||||
import net.corda.core.utilities.days
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.services.*
|
||||
import net.corda.core.node.services.vault.*
|
||||
import net.corda.core.node.services.vault.QueryCriteria.*
|
||||
import net.corda.core.utilities.seconds
|
||||
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.core.utilities.*
|
||||
import net.corda.node.utilities.CordaPersistence
|
||||
import net.corda.node.utilities.configureDatabase
|
||||
import net.corda.schemas.CashSchemaV1
|
||||
@ -27,7 +23,7 @@ import net.corda.schemas.SampleCashSchemaV3
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.contracts.*
|
||||
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.schemas.DummyLinearStateSchemaV1
|
||||
import org.assertj.core.api.Assertions
|
||||
@ -54,24 +50,9 @@ class VaultQueryTests : TestDependencyInjectionBase() {
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
val dataSourceProps = makeTestDataSourceProperties()
|
||||
database = configureDatabase(dataSourceProps, makeTestDatabaseProperties())
|
||||
database.transaction {
|
||||
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)
|
||||
}
|
||||
}
|
||||
val databaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(MEGA_CORP_KEY))
|
||||
database = databaseAndServices.first
|
||||
services = databaseAndServices.second
|
||||
}
|
||||
|
||||
@After
|
||||
@ -97,7 +78,7 @@ class VaultQueryTests : TestDependencyInjectionBase() {
|
||||
_database.transaction {
|
||||
|
||||
// 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 linearStatesJKL = services.fillWithSomeTestLinearStates(2, "JKL")
|
||||
services.fillWithSomeTestLinearStates(3, "ABC")
|
||||
@ -239,15 +220,15 @@ class VaultQueryTests : TestDependencyInjectionBase() {
|
||||
fun `unconsumed cash states sorted by state ref`() {
|
||||
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 issuedStateRefs = issuedStates.states.map { it.ref }.toList()
|
||||
stateRefs.addAll(issuedStateRefs)
|
||||
|
||||
val spentStates = services.consumeCash(25.DOLLARS)
|
||||
var consumedStateRefs = spentStates.consumed.map { it.ref }.toList()
|
||||
var producedStateRefs = spentStates.produced.map { it.ref }.toList()
|
||||
val consumedStateRefs = spentStates.consumed.map { it.ref }.toList()
|
||||
val producedStateRefs = spentStates.produced.map { it.ref }.toList()
|
||||
stateRefs.addAll(consumedStateRefs.plus(producedStateRefs))
|
||||
|
||||
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`() {
|
||||
database.transaction {
|
||||
services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 10, 10, Random(0L))
|
||||
services.consumeCash(10.DOLLARS)
|
||||
services.consumeCash(10.DOLLARS)
|
||||
val consumed = mutableSetOf<SecureHash>()
|
||||
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 sortAttributeIndex = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF_INDEX)
|
||||
@ -283,13 +265,11 @@ class VaultQueryTests : TestDependencyInjectionBase() {
|
||||
|
||||
results.statesMetadata.forEach {
|
||||
println(" ${it.ref}")
|
||||
assertThat(it.status).isEqualTo(Vault.StateStatus.UNCONSUMED)
|
||||
}
|
||||
|
||||
// explicit sort order asc by txnId and then index:
|
||||
// order by
|
||||
// vaultschem1_.transaction_id asc,
|
||||
// vaultschem1_.output_index asc
|
||||
assertThat(results.states).hasSize(9) // -2 CONSUMED + 1 NEW UNCONSUMED (change)
|
||||
val sorted = results.states.sortedBy { it.ref.toString() }
|
||||
assertThat(results.states).isEqualTo(sorted)
|
||||
assertThat(results.states).allSatisfy { !consumed.contains(it.ref.txhash) }
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@Test
|
||||
@ -870,7 +850,7 @@ class VaultQueryTests : TestDependencyInjectionBase() {
|
||||
fun `aggregate functions count by contract type and state status`() {
|
||||
database.transaction {
|
||||
// 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 linearStatesJKL = services.fillWithSomeTestLinearStates(2, "JKL")
|
||||
services.fillWithSomeTestLinearStates(3, "ABC")
|
||||
@ -896,14 +876,14 @@ class VaultQueryTests : TestDependencyInjectionBase() {
|
||||
services.consumeLinearStates(linearStatesXYZ.states.toList())
|
||||
services.consumeLinearStates(linearStatesJKL.states.toList())
|
||||
services.consumeDeals(dealStates.states.filter { it.state.data.ref == "456" })
|
||||
services.consumeCash(50.DOLLARS)
|
||||
val cashUpdates = services.consumeCash(50.DOLLARS)
|
||||
|
||||
// UNCONSUMED states (default)
|
||||
|
||||
// count fungible assets
|
||||
val countCriteriaUnconsumed = QueryCriteria.VaultCustomQueryCriteria(count, Vault.StateStatus.UNCONSUMED)
|
||||
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
|
||||
val linearStateCountUnconsumed = vaultQuerySvc.queryBy<LinearState>(countCriteriaUnconsumed).otherResults.single() as Long
|
||||
@ -918,7 +898,7 @@ class VaultQueryTests : TestDependencyInjectionBase() {
|
||||
// count fungible assets
|
||||
val countCriteriaConsumed = QueryCriteria.VaultCustomQueryCriteria(count, Vault.StateStatus.CONSUMED)
|
||||
val fungibleStateCountConsumed = vaultQuerySvc.queryBy<FungibleAsset<*>>(countCriteriaConsumed).otherResults.single() as Long
|
||||
assertThat(fungibleStateCountConsumed).isEqualTo(6L)
|
||||
assertThat(fungibleStateCountConsumed.toInt()).isEqualTo(cashUpdates.consumed.size)
|
||||
|
||||
// count linear states
|
||||
val linearStateCountConsumed = vaultQuerySvc.queryBy<LinearState>(countCriteriaConsumed).otherResults.single() as Long
|
||||
@ -962,7 +942,7 @@ class VaultQueryTests : TestDependencyInjectionBase() {
|
||||
fun `states consumed after time`() {
|
||||
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.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.vault.QueryCriteria
|
||||
import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
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.configureDatabase
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.contracts.*
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.node.makeTestDataSourceProperties
|
||||
import net.corda.testing.node.makeTestDatabaseProperties
|
||||
import net.corda.testing.node.makeTestDatabaseAndMockServices
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.junit.After
|
||||
@ -44,23 +39,9 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
|
||||
@Before
|
||||
fun setUp() {
|
||||
LogHelper.setLevel(VaultWithCashTest::class)
|
||||
val dataSourceProps = makeTestDataSourceProperties()
|
||||
database = configureDatabase(dataSourceProps, makeTestDatabaseProperties())
|
||||
database.transaction {
|
||||
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)
|
||||
}
|
||||
}
|
||||
val databaseAndServices = makeTestDatabaseAndMockServices()
|
||||
database = databaseAndServices.first
|
||||
services = databaseAndServices.second
|
||||
}
|
||||
|
||||
@After
|
||||
@ -103,7 +84,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
|
||||
|
||||
// A tx that spends our money.
|
||||
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 spendTX = notaryServices.addSignature(spendPTX)
|
||||
|
||||
@ -151,7 +132,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
|
||||
database.transaction {
|
||||
try {
|
||||
val txn1Builder = TransactionBuilder(DUMMY_NOTARY)
|
||||
vault.generateSpend(txn1Builder, 60.DOLLARS, BOB)
|
||||
Cash.generateSpend(services, txn1Builder, 60.DOLLARS, BOB)
|
||||
val ptxn1 = notaryServices.signInitialTransaction(txn1Builder)
|
||||
val txn1 = services.addSignature(ptxn1, freshKey)
|
||||
println("txn1: ${txn1.id} spent ${((txn1.tx.outputs[0].data) as Cash.State).amount}")
|
||||
@ -187,7 +168,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
|
||||
database.transaction {
|
||||
try {
|
||||
val txn2Builder = TransactionBuilder(DUMMY_NOTARY)
|
||||
vault.generateSpend(txn2Builder, 80.DOLLARS, BOB)
|
||||
Cash.generateSpend(services, txn2Builder, 80.DOLLARS, BOB)
|
||||
val ptxn2 = notaryServices.signInitialTransaction(txn2Builder)
|
||||
val txn2 = services.addSignature(ptxn2, freshKey)
|
||||
println("txn2: ${txn2.id} spent ${((txn2.tx.outputs[0].data) as Cash.State).amount}")
|
||||
@ -299,7 +280,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
|
||||
database.transaction {
|
||||
// A tx that spends our money.
|
||||
val spendTXBuilder = TransactionBuilder(DUMMY_NOTARY)
|
||||
vault.generateSpend(spendTXBuilder, 80.DOLLARS, BOB)
|
||||
Cash.generateSpend(services, spendTXBuilder, 80.DOLLARS, BOB)
|
||||
val spendPTX = notaryServices.signInitialTransaction(spendTXBuilder)
|
||||
val spendTX = services.addSignature(spendPTX, freshKey)
|
||||
services.recordTransactions(spendTX)
|
||||
|
@ -34,7 +34,7 @@ class DummyIssueAndMove(private val notary: Party, private val counterpartyNode:
|
||||
// Move ownership of the asset to the counterparty
|
||||
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
|
||||
signInitialTransaction(moveTxBuilder, keys)
|
||||
}
|
||||
|
@ -13,9 +13,9 @@ import net.corda.core.identity.Party
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.toFuture
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.testing.CHARLIE
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
import net.corda.testing.DUMMY_NOTARY_KEY
|
||||
@ -228,10 +228,11 @@ fun ServiceHub.evolveLinearState(linearState: StateAndRef<LinearState>) : StateA
|
||||
@JvmOverloads
|
||||
fun ServiceHub.consumeCash(amount: Amount<Currency>, to: Party = CHARLIE): Vault.Update<ContractState> {
|
||||
val update = vaultService.rawUpdates.toFuture()
|
||||
val services = this
|
||||
|
||||
// A tx that spends our money.
|
||||
val spendTX = TransactionBuilder(DUMMY_NOTARY).apply {
|
||||
vaultService.generateSpend(this, amount, to)
|
||||
Cash.generateSpend(services, this, amount, to)
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
}.toSignedTransaction(checkSufficientSignatures = false)
|
||||
|
||||
|
@ -9,6 +9,7 @@ import net.corda.core.messaging.DataFeed
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.services.*
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.serialization.SerializeAsToken
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
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.NodeSchemaService
|
||||
import net.corda.node.services.transactions.InMemoryTransactionVerifierService
|
||||
import net.corda.node.services.vault.HibernateVaultQueryImpl
|
||||
import net.corda.node.services.vault.NodeVaultService
|
||||
import net.corda.testing.DUMMY_CA
|
||||
import net.corda.testing.MEGA_CORP
|
||||
import net.corda.testing.MOCK_IDENTITIES
|
||||
import net.corda.testing.getTestPartyAndCertificate
|
||||
import net.corda.node.utilities.CordaPersistence
|
||||
import net.corda.node.utilities.configureDatabase
|
||||
import net.corda.schemas.CashSchemaV1
|
||||
import net.corda.schemas.CommercialPaperSchemaV1
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.schemas.DummyLinearStateSchemaV1
|
||||
import org.bouncycastle.operator.ContentSigner
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
@ -212,4 +216,29 @@ fun makeTestDatabaseProperties(): Properties {
|
||||
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")
|
||||
|
Loading…
Reference in New Issue
Block a user