mirror of
https://github.com/corda/corda.git
synced 2025-03-22 03:55:26 +00:00
Merge pull request #1184 from corda/mnesbit-generic-generatespend
Move generateSpend onto Cash contract. Add fully generic replacement.
This commit is contained in:
commit
3bc7914a09
@ -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…
x
Reference in New Issue
Block a user