Make a generic generate spend. Tests pass.

Fixup after rebase

Fixup after rebase

Make node have only test compile dependency against finance module.

Use original query code.

Fixup after rebase

Update docs

Edit docs

Add to changelog

Follow recommendations from PR

Follow recommendations from PR

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

Tweak a few comments

Don't include tryLockFungibleStateForSpending in soft lock docs.

Respond to PR comments

Fix whitespace error

Fix compile error

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

View File

@ -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

View File

@ -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<*, *>>>()

View File

@ -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

View File

@ -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))
}
}

View File

@ -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

View File

@ -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()
}

View File

@ -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"))

View File

@ -11,17 +11,12 @@ import net.corda.core.node.services.VaultService
import net.corda.core.node.services.queryBy
import net.corda.core.node.services.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)