Adapted all Contract tests that perform Cash Spending to use the Vault Service.

Note: pending resolution of 2 failing tests (IRSSimulation, CP issue, move & redeem)
This commit is contained in:
Jose Coll 2016-10-19 10:38:14 +01:00
parent c23aea3997
commit ef2ff777a7
8 changed files with 236 additions and 199 deletions

View File

@ -7,6 +7,7 @@ import com.r3corda.core.contracts.Timestamp;
import com.r3corda.core.contracts.TransactionForContract.*;
import com.r3corda.core.contracts.clauses.*;
import com.r3corda.core.crypto.*;
import com.r3corda.core.node.services.*;
import com.r3corda.core.transactions.*;
import kotlin.*;
import org.jetbrains.annotations.*;
@ -304,8 +305,8 @@ public class JavaCommercialPaper implements Contract {
return new TransactionType.General.Builder(notary).withItems(output, new Command(new Commands.Issue(), issuance.getParty().getOwningKey()));
}
public void generateRedeem(TransactionBuilder tx, StateAndRef<State> paper, List<StateAndRef<Cash.State>> vault) throws InsufficientBalanceException {
new Cash().generateSpend(tx, StructuresKt.withoutIssuer(paper.getState().getData().getFaceValue()), paper.getState().getData().getOwner(), vault, null);
public void generateRedeem(TransactionBuilder tx, StateAndRef<State> paper, VaultService vault) throws InsufficientBalanceException {
vault.generateSpend(tx, StructuresKt.withoutIssuer(paper.getState().getData().getFaceValue()), paper.getState().getData().getOwner(), null);
tx.addInputState(paper);
tx.addCommand(new Command(new Commands.Redeem(), paper.getState().getData().getOwner()));
}

View File

@ -14,6 +14,7 @@ import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.toBase58String
import com.r3corda.core.crypto.toStringShort
import com.r3corda.core.node.services.VaultService
import com.r3corda.core.random63BitValue
import com.r3corda.core.schemas.MappedSchema
import com.r3corda.core.schemas.PersistentState
@ -218,10 +219,10 @@ class CommercialPaper : Contract {
* @throws InsufficientBalanceException if the vault doesn't contain enough money to pay the redeemer.
*/
@Throws(InsufficientBalanceException::class)
fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef<State>, vault: List<StateAndRef<Cash.State>>) {
fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef<State>, vault: VaultService) {
// Add the cash movement using the states in our vault.
val amount = paper.state.data.faceValue.let { amount -> Amount(amount.quantity, amount.token.product) }
Cash().generateSpend(tx, amount, paper.state.data.owner, vault)
vault.generateSpend(tx, amount, paper.state.data.owner)
tx.addInputState(paper)
tx.addCommand(CommercialPaper.Commands.Redeem(), paper.state.data.owner)
}

View File

@ -9,6 +9,7 @@ import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.toStringShort
import com.r3corda.core.node.services.Vault
import com.r3corda.core.node.services.VaultService
import com.r3corda.core.transactions.TransactionBuilder
import com.r3corda.core.utilities.Emoji
import java.security.PublicKey
@ -125,10 +126,9 @@ class CommercialPaperLegacy : Contract {
}
@Throws(InsufficientBalanceException::class)
fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef<State>, vault: Vault) {
fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef<State>, vault: VaultService) {
// Add the cash movement using the states in our vault.
Cash().generateSpend(tx, paper.state.data.faceValue.withoutIssuer(),
paper.state.data.owner, vault.statesOfType<Cash.State>())
vault.generateSpend(tx, paper.state.data.faceValue.withoutIssuer(), paper.state.data.owner)
tx.addInputState(paper)
tx.addCommand(Command(Commands.Redeem(), paper.state.data.owner))
}

View File

@ -47,25 +47,6 @@ abstract class OnLedgerAsset<T : Any, C: CommandData, S : FungibleAsset<T>> : Co
generateExitCommand = { amount -> generateExitCommand(amount) }
)
/**
* Generate a transaction that consumes one or more of the given input states to move assets to the given pubkey.
* Note that the vault is not updated: it's up to you to do that.
*
* @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.
*/
@Throws(InsufficientBalanceException::class)
fun generateSpend(tx: TransactionBuilder,
amount: Amount<T>,
to: PublicKey,
assetsStates: List<StateAndRef<S>>,
onlyFromParties: Set<Party>? = null): List<PublicKey>
= conserveClause.generateSpend(tx, amount, to, assetsStates, onlyFromParties,
deriveState = { state, amount, owner -> deriveState(state, amount, owner) },
generateMoveCommand = { generateMoveCommand() })
abstract fun generateExitCommand(amount: Amount<Issued<T>>): FungibleAsset.Commands.Exit<T>
abstract fun generateIssueCommand(): FungibleAsset.Commands.Issue
abstract fun generateMoveCommand(): FungibleAsset.Commands.Move

View File

@ -85,90 +85,6 @@ abstract class AbstractConserveAmount<S : FungibleAsset<T>, C : CommandData, T :
return amountIssued.token.issuer.party.owningKey
}
/**
* Generate a transaction that consumes one or more of the given input states to move assets to the given pubkey.
* Note that the vault is not updated: it's up to you to do that.
*
* @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.
*/
@Throws(InsufficientBalanceException::class)
fun generateSpend(tx: TransactionBuilder,
amount: Amount<T>,
to: PublicKey,
assetsStates: List<StateAndRef<S>>,
onlyFromParties: Set<Party>? = null,
deriveState: (TransactionState<S>, Amount<Issued<T>>, PublicKey) -> TransactionState<S>,
generateMoveCommand: () -> CommandData): List<PublicKey> {
// Discussion
//
// This code is analogous to the Wallet.send() set of methods in bitcoinj, and has the same general outline.
//
// First we must select a set of asset states (which for convenience we will call 'coins' here, as in bitcoinj).
// The input states can be considered our "vault", and may consist of different products, and with different
// issuers and deposits.
//
// Coin selection is a complex problem all by itself and many different approaches can be used. It is easily
// possible for different actors to use different algorithms and approaches that, for example, compete on
// privacy vs efficiency (number of states created). Some spends may be artificial just for the purposes of
// obfuscation and so on.
//
// Having selected input states of the correct asset, we must craft output states for the amount we're sending and
// the "change", which goes back to us. The change is required to make the amounts balance. We may need more
// than one change output in order to avoid merging assets from different deposits. The point of this design
// is to ensure that ledger entries are immutable and globally identifiable.
//
// Finally, we add the states to the provided partial transaction.
val currency = amount.token
var acceptableCoins = run {
val ofCurrency = assetsStates.filter { it.state.data.amount.token.product == currency }
if (onlyFromParties != null)
ofCurrency.filter { it.state.data.deposit.party in onlyFromParties }
else
ofCurrency
}
tx.notary = acceptableCoins.firstOrNull()?.state?.notary
// TODO: We should be prepared to produce multiple transactions spending inputs from
// different notaries, or at least group states by notary and take the set with the
// highest total value
acceptableCoins = acceptableCoins.filter { it.state.notary == tx.notary }
val (gathered, gatheredAmount) = gatherCoins(acceptableCoins, amount)
val takeChangeFrom = gathered.firstOrNull()
val change = if (takeChangeFrom != null && gatheredAmount > amount) {
Amount(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.data.issuanceDef)
} else {
null
}
val keysUsed = gathered.map { it.state.data.owner }.toSet()
val states = gathered.groupBy { it.state.data.deposit }.map {
val coins = it.value
val totalAmount = coins.map { it.state.data.amount }.sumOrThrow()
deriveState(coins.first().state, totalAmount, to)
}
val outputs = if (change != null) {
// Just copy a key across as the change key. In real life of course, this works but leaks private data.
// In bitcoinj we derive a fresh key here and then shuffle the outputs to ensure it's hard to follow
// value flows through the transaction graph.
val changeKey = gathered.first().state.data.owner
// Add a change output and adjust the last output downwards.
states.subList(0, states.lastIndex) +
states.last().let { deriveState(it, it.data.amount - change, it.data.owner) } +
deriveState(gathered.last().state, change, changeKey)
} else states
for (state in gathered) tx.addInputState(state)
for (state in outputs) tx.addOutputState(state)
// What if we already have a move command with the right keys? Filter it out here or in platform code?
val keysList = keysUsed.toList()
tx.addCommand(generateMoveCommand(), keysList)
return keysList
}
override fun verify(tx: TransactionForContract,
inputs: List<S>,
outputs: List<S>,

View File

@ -6,6 +6,7 @@ import com.r3corda.contracts.asset.DUMMY_CASH_ISSUER
import com.r3corda.contracts.asset.DUMMY_CASH_ISSUER_KEY
import com.r3corda.core.contracts.Amount
import com.r3corda.core.contracts.Issued
import com.r3corda.core.contracts.PartyAndReference
import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.core.contracts.TransactionType
import com.r3corda.core.crypto.Party
@ -14,6 +15,7 @@ import com.r3corda.core.node.services.Vault
import com.r3corda.core.protocols.StateMachineRunId
import com.r3corda.core.serialization.OpaqueBytes
import com.r3corda.core.utilities.DUMMY_NOTARY
import java.security.KeyPair
import java.security.PublicKey
import java.util.*
@ -34,7 +36,9 @@ fun ServiceHub.fillWithSomeTestCash(howMuch: Amount<Currency>,
atMostThisManyStates: Int = 10,
rng: Random = Random(),
ref: OpaqueBytes = OpaqueBytes(ByteArray(1, { 1 })),
ownedBy: PublicKey? = null): Vault {
ownedBy: PublicKey? = null,
issuedBy: PartyAndReference = DUMMY_CASH_ISSUER,
issuerKey: KeyPair = DUMMY_CASH_ISSUER_KEY): Vault {
val amounts = calculateRandomlySizedAmounts(howMuch, atLeastThisManyStates, atMostThisManyStates, rng)
val myKey: PublicKey = ownedBy ?: myInfo.legalIdentity.owningKey
@ -43,8 +47,8 @@ fun ServiceHub.fillWithSomeTestCash(howMuch: Amount<Currency>,
val cash = Cash()
val transactions: List<SignedTransaction> = amounts.map { pennies ->
val issuance = TransactionType.General.Builder(null)
cash.generateIssue(issuance, Amount(pennies, Issued(DUMMY_CASH_ISSUER.copy(reference = ref), howMuch.token)), myKey, outputNotary)
issuance.signWith(DUMMY_CASH_ISSUER_KEY)
cash.generateIssue(issuance, Amount(pennies, Issued(issuedBy.copy(reference = ref), howMuch.token)), myKey, outputNotary)
issuance.signWith(issuerKey)
return@map issuance.toSignedTransaction(true)
}

View File

@ -7,6 +7,7 @@ import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.days
import com.r3corda.core.node.recordTransactions
import com.r3corda.core.node.services.VaultService
import com.r3corda.core.seconds
import com.r3corda.core.transactions.LedgerTransaction
import com.r3corda.core.transactions.SignedTransaction
@ -14,8 +15,12 @@ import com.r3corda.core.utilities.DUMMY_NOTARY
import com.r3corda.core.utilities.DUMMY_NOTARY_KEY
import com.r3corda.core.utilities.DUMMY_PUBKEY_1
import com.r3corda.core.utilities.TEST_TX_TIME
import com.r3corda.node.services.vault.NodeVaultService
import com.r3corda.node.utilities.configureDatabase
import com.r3corda.node.utilities.databaseTransaction
import com.r3corda.testing.node.MockServices
import com.r3corda.testing.*
import com.r3corda.testing.node.makeTestDataSourceProperties
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
@ -199,11 +204,38 @@ class CommercialPaperTestsGeneric {
@Test
fun `issue move and then redeem`() {
val aliceServices = MockServices()
val alicesVault = aliceServices.fillWithSomeTestCash(9000.DOLLARS)
val bigCorpServices = MockServices()
val bigCorpVault = bigCorpServices.fillWithSomeTestCash(13000.DOLLARS)
val dataSourceAndDatabase = configureDatabase(makeTestDataSourceProperties())
val database = dataSourceAndDatabase.second
databaseTransaction(database) {
val aliceServices = object : MockServices() {
override val vaultService: VaultService = NodeVaultService(this)
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
for (stx in txs) {
storageService.validatedTransactions.addTransaction(stx)
}
// Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions.
vaultService.notifyAll(txs.map { it.tx })
}
}
val alicesVault = aliceServices.fillWithSomeTestCash(9000.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1)
val aliceVaultService = aliceServices.vaultService
val bigCorpServices = object : MockServices() {
override val vaultService: VaultService = NodeVaultService(this)
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
for (stx in txs) {
storageService.validatedTransactions.addTransaction(stx)
}
// Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions.
vaultService.notifyAll(txs.map { it.tx })
}
}
val bigCorpVault = bigCorpServices.fillWithSomeTestCash(13000.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1)
val bigCorpVaultService = bigCorpServices.vaultService
// Propagate the cash transactions to each side.
aliceServices.recordTransactions(bigCorpVault.states.map { bigCorpServices.storageService.validatedTransactions.getTransaction(it.ref.txhash)!! })
@ -222,7 +254,7 @@ class CommercialPaperTestsGeneric {
// Alice pays $9000 to BigCorp to own some of their debt.
val moveTX: SignedTransaction = run {
val ptx = TransactionType.General.Builder(DUMMY_NOTARY)
Cash().generateSpend(ptx, 9000.DOLLARS, bigCorpServices.key.public, alicesVault.statesOfType<Cash.State>())
aliceVaultService.generateSpend(ptx, 9000.DOLLARS, bigCorpServices.key.public)
CommercialPaper().generateMove(ptx, issueTX.tx.outRef(0), aliceServices.key.public)
ptx.signWith(bigCorpServices.key)
ptx.signWith(aliceServices.key)
@ -233,7 +265,7 @@ class CommercialPaperTestsGeneric {
fun makeRedeemTX(time: Instant): SignedTransaction {
val ptx = TransactionType.General.Builder(DUMMY_NOTARY)
ptx.setTime(time, 30.seconds)
CommercialPaper().generateRedeem(ptx, moveTX.tx.outRef(1), bigCorpVault.statesOfType<Cash.State>())
CommercialPaper().generateRedeem(ptx, moveTX.tx.outRef(1), bigCorpVaultService)
ptx.signWith(aliceServices.key)
ptx.signWith(bigCorpServices.key)
ptx.signWith(DUMMY_NOTARY_KEY)
@ -257,4 +289,5 @@ class CommercialPaperTestsGeneric {
validRedemption.toLedgerTransaction(aliceServices).verify()
}
}
}

View File

@ -1,15 +1,32 @@
package com.r3corda.contracts.asset
import com.r3corda.contracts.testing.fillWithSomeTestCash
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.generateKeyPair
import com.r3corda.core.node.services.Vault
import com.r3corda.core.node.services.VaultService
import com.r3corda.core.serialization.OpaqueBytes
import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.core.transactions.WireTransaction
import com.r3corda.core.utilities.DUMMY_NOTARY
import com.r3corda.core.utilities.DUMMY_PUBKEY_1
import com.r3corda.core.utilities.DUMMY_PUBKEY_2
import com.r3corda.core.utilities.LogHelper
import com.r3corda.node.services.vault.NodeVaultService
import com.r3corda.node.utilities.configureDatabase
import com.r3corda.node.utilities.databaseTransaction
import com.r3corda.testing.*
import com.r3corda.testing.node.MockKeyManagementService
import com.r3corda.testing.node.MockServices
import com.r3corda.testing.node.makeTestDataSourceProperties
import org.jetbrains.exposed.sql.Database
import org.junit.After
import org.junit.Before
import org.junit.Test
import java.io.Closeable
import java.security.KeyPair
import java.security.PublicKey
import java.util.*
import kotlin.test.*
@ -29,6 +46,52 @@ class CashTests {
amount = Amount(amount.quantity, token = amount.token.copy(deposit.copy(reference = OpaqueBytes.of(ref))))
)
lateinit var services: MockServices
val vault: VaultService get() = services.vaultService
lateinit var dataSource: Closeable
lateinit var database: Database
lateinit var VAULT: Vault
@Before
fun setUp() {
LogHelper.setLevel(NodeVaultService::class)
val dataSourceAndDatabase = configureDatabase(makeTestDataSourceProperties())
dataSource = dataSourceAndDatabase.first
database = dataSourceAndDatabase.second
databaseTransaction(database) {
services = object : MockServices() {
override val keyManagementService: MockKeyManagementService = MockKeyManagementService(MINI_CORP_KEY, MEGA_CORP_KEY, OUR_KEY)
override val vaultService: VaultService = NodeVaultService(this)
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
for (stx in txs) {
storageService.validatedTransactions.addTransaction(stx)
}
// Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions.
vaultService.notifyAll(txs.map { it.tx })
}
}
services.fillWithSomeTestCash(howMuch = 100.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1,
issuedBy = MEGA_CORP.ref(1), issuerKey = MEGA_CORP_KEY, ownedBy = OUR_PUBKEY_1)
services.fillWithSomeTestCash(howMuch = 400.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1,
issuedBy = MEGA_CORP.ref(1), issuerKey = MEGA_CORP_KEY, ownedBy = OUR_PUBKEY_1)
services.fillWithSomeTestCash(howMuch = 80.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1,
issuedBy = MINI_CORP.ref(1), issuerKey = MINI_CORP_KEY, ownedBy = OUR_PUBKEY_1)
services.fillWithSomeTestCash(howMuch = 80.SWISS_FRANCS, atLeastThisManyStates = 1, atMostThisManyStates = 1,
issuedBy = MINI_CORP.ref(1), issuerKey = MINI_CORP_KEY, ownedBy = OUR_PUBKEY_1)
VAULT = services.vaultService.currentVault
}
}
@After
fun tearDown() {
LogHelper.reset(NodeVaultService::class)
dataSource.close()
}
@Test
fun trivial() {
transaction {
@ -402,7 +465,9 @@ class CashTests {
//
// Spend tx generation
val OUR_PUBKEY_1 = DUMMY_PUBKEY_1
val OUR_KEY: KeyPair by lazy { generateKeyPair() }
val OUR_PUBKEY_1: PublicKey get() = OUR_KEY.public
val THEIR_PUBKEY_1 = DUMMY_PUBKEY_2
fun makeCash(amount: Amount<Currency>, corp: Party, depositRef: Byte = 1) =
@ -428,8 +493,10 @@ class CashTests {
}
fun makeSpend(amount: Amount<Currency>, dest: PublicKey): WireTransaction {
val tx = TransactionType.General.Builder(DUMMY_NOTARY)
Cash().generateSpend(tx, amount, dest, WALLET)
var tx = TransactionType.General.Builder(DUMMY_NOTARY)
databaseTransaction(database) {
vault.generateSpend(tx, amount, dest)
}
return tx.toWireTransaction()
}
@ -485,51 +552,84 @@ class CashTests {
@Test
fun generateSimpleDirectSpend() {
databaseTransaction(database) {
val wtx = makeSpend(100.DOLLARS, THEIR_PUBKEY_1)
assertEquals(WALLET[0].ref, wtx.inputs[0])
assertEquals(WALLET[0].state.data.copy(owner = THEIR_PUBKEY_1), wtx.outputs[0].data)
val vaultState = VAULT.states.elementAt(0) as StateAndRef<Cash.State>
assertEquals(vaultState.ref, wtx.inputs[0])
assertEquals(vaultState.state.data.copy(owner = THEIR_PUBKEY_1), wtx.outputs[0].data)
assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
}
}
@Test
fun generateSimpleSpendWithParties() {
databaseTransaction(database) {
val tx = TransactionType.General.Builder(DUMMY_NOTARY)
Cash().generateSpend(tx, 80.DOLLARS, ALICE_PUBKEY, WALLET, setOf(MINI_CORP))
assertEquals(WALLET[2].ref, tx.inputStates()[0])
vault.generateSpend(tx, 80.DOLLARS, ALICE_PUBKEY, setOf(MINI_CORP))
assertEquals(VAULT.states.elementAt(2).ref, tx.inputStates()[0])
}
}
@Test
fun generateSimpleSpendWithChange() {
databaseTransaction(database) {
val wtx = makeSpend(10.DOLLARS, THEIR_PUBKEY_1)
assertEquals(WALLET[0].ref, wtx.inputs[0])
assertEquals(WALLET[0].state.data.copy(owner = THEIR_PUBKEY_1, amount = 10.DOLLARS `issued by` defaultIssuer), wtx.outputs[0].data)
assertEquals(WALLET[0].state.data.copy(amount = 90.DOLLARS `issued by` defaultIssuer), wtx.outputs[1].data)
val vaultState = VAULT.states.elementAt(0) as StateAndRef<Cash.State>
assertEquals(vaultState.ref, wtx.inputs[0])
assertEquals(vaultState.state.data.copy(owner = THEIR_PUBKEY_1, amount = 10.DOLLARS `issued by` defaultIssuer), wtx.outputs[0].data)
assertEquals(vaultState.state.data.copy(amount = 90.DOLLARS `issued by` defaultIssuer), wtx.outputs[1].data)
assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
}
}
@Test
fun generateSpendWithTwoInputs() {
databaseTransaction(database) {
val wtx = makeSpend(500.DOLLARS, THEIR_PUBKEY_1)
assertEquals(WALLET[0].ref, wtx.inputs[0])
assertEquals(WALLET[1].ref, wtx.inputs[1])
assertEquals(WALLET[0].state.data.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.outputs[0].data)
val vaultState0 = VAULT.states.elementAt(0) as StateAndRef<Cash.State>
val vaultState1 = VAULT.states.elementAt(1)
assertEquals(vaultState0.ref, wtx.inputs[0])
assertEquals(vaultState1.ref, wtx.inputs[1])
assertEquals(vaultState0.state.data.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.outputs[0].data)
assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
}
}
@Test
fun generateSpendMixedDeposits() {
databaseTransaction(database) {
val wtx = makeSpend(580.DOLLARS, THEIR_PUBKEY_1)
assertEquals(3, wtx.inputs.size)
assertEquals(WALLET[0].ref, wtx.inputs[0])
assertEquals(WALLET[1].ref, wtx.inputs[1])
assertEquals(WALLET[2].ref, wtx.inputs[2])
assertEquals(WALLET[0].state.data.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.outputs[0].data)
assertEquals(WALLET[2].state.data.copy(owner = THEIR_PUBKEY_1), wtx.outputs[1].data)
val vaultState0 = VAULT.states.elementAt(0) as StateAndRef<Cash.State>
val vaultState1 = VAULT.states.elementAt(1)
val vaultState2 = VAULT.states.elementAt(2) as StateAndRef<Cash.State>
assertEquals(vaultState0.ref, wtx.inputs[0])
assertEquals(vaultState1.ref, wtx.inputs[1])
assertEquals(vaultState2.ref, wtx.inputs[2])
assertEquals(vaultState0.state.data.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.outputs[0].data)
assertEquals(vaultState2.state.data.copy(owner = THEIR_PUBKEY_1), wtx.outputs[1].data)
assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
}
}
@Test
fun generateSpendInsufficientBalance() {
databaseTransaction(database) {
val e: InsufficientBalanceException = assertFailsWith("balance") {
makeSpend(1000.DOLLARS, THEIR_PUBKEY_1)
}
@ -539,6 +639,7 @@ class CashTests {
makeSpend(81.SWISS_FRANCS, THEIR_PUBKEY_1)
}
}
}
/**
* Confirm that aggregation of states is correctly modelled.