diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt index 4e0b5ce720..4cdb1da19a 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt @@ -32,7 +32,7 @@ import java.util.* import kotlin.test.* class CashTests { - val defaultRef = OpaqueBytes(ByteArray(1, {1})) + val defaultRef = OpaqueBytes(ByteArray(1, { 1 })) val defaultIssuer = MEGA_CORP.ref(defaultRef) val inState = Cash.State( amount = 1000.DOLLARS `issued by` defaultIssuer, @@ -85,12 +85,6 @@ class CashTests { } } - @After - fun tearDown() { - LogHelper.reset(NodeVaultService::class) - dataSource.close() - } - @Test fun trivial() { transaction { @@ -264,7 +258,7 @@ class CashTests { // Include the previously issued cash in a new issuance command ptx = TransactionType.General.Builder(DUMMY_NOTARY) ptx.addInputState(tx.tx.outRef<Cash.State>(0)) - Cash().generateIssue(ptx, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY) + Cash().generateIssue(ptx, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY) } @Test @@ -492,7 +486,7 @@ class CashTests { } fun makeSpend(amount: Amount<Currency>, dest: PublicKey): WireTransaction { - var tx = TransactionType.General.Builder(DUMMY_NOTARY) + val tx = TransactionType.General.Builder(DUMMY_NOTARY) databaseTransaction(database) { vault.generateSpend(tx, amount, dest) } @@ -618,8 +612,8 @@ class CashTests { 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(vaultState0.state.data.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.outputs[1].data) + assertEquals(vaultState2.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]) } } diff --git a/core/src/main/kotlin/com/r3corda/core/node/services/Services.kt b/core/src/main/kotlin/com/r3corda/core/node/services/Services.kt index 322517c859..b28c1b97a0 100644 --- a/core/src/main/kotlin/com/r3corda/core/node/services/Services.kt +++ b/core/src/main/kotlin/com/r3corda/core/node/services/Services.kt @@ -165,7 +165,9 @@ interface VaultService { } /** - * Fungible Asset operations + * [InsufficientBalanceException] is thrown when a Cash Spending transaction fails because + * there is insufficient quantity for a given currency (and optionally set of Issuer Parties). + * Note: an [Amount] of [Currency] is only fungible for a given Issuer Party within a [FungibleAsset] **/ @Throws(InsufficientBalanceException::class) fun generateSpend(tx: TransactionBuilder, diff --git a/node/src/main/kotlin/com/r3corda/node/internal/ServerRPCOps.kt b/node/src/main/kotlin/com/r3corda/node/internal/ServerRPCOps.kt index 38b1ec0a7f..143a6b3c5f 100644 --- a/node/src/main/kotlin/com/r3corda/node/internal/ServerRPCOps.kt +++ b/node/src/main/kotlin/com/r3corda/node/internal/ServerRPCOps.kt @@ -79,7 +79,8 @@ class ServerRPCOps( val builder: TransactionBuilder = TransactionType.General.Builder(null) // TODO: Have some way of restricting this to states the caller controls try { - val (spendTX, keysForSigning) = services.vaultService.generateSpend(builder, req.amount.withoutIssuer(), req.recipient.owningKey) + val (spendTX, keysForSigning) = services.vaultService.generateSpend(builder, + req.amount.withoutIssuer(), req.recipient.owningKey, setOf(req.amount.token.issuer.party)) keysForSigning.forEach { val key = services.keyManagementService.keys[it] ?: throw IllegalStateException("Could not find signing key for ${it.toStringShort()}") diff --git a/node/src/main/kotlin/com/r3corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/com/r3corda/node/services/vault/NodeVaultService.kt index 425ac15549..883df42e4f 100644 --- a/node/src/main/kotlin/com/r3corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/com/r3corda/node/services/vault/NodeVaultService.kt @@ -170,17 +170,22 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT val coins = it.value val totalAmount = coins.map { it.state.data.amount }.sumOrThrow() deriveState(coins.first().state, totalAmount, to) - } + }.sortedBy { it.data.amount.quantity } 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 + val existingOwner = 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) + states.last().let { + val spent = it.data.amount.withoutIssuer() - change.withoutIssuer() + deriveState(it, Amount(spent.quantity, it.data.amount.token), it.data.owner) + } + + states.last().let { + deriveState(it, Amount(change.quantity, it.data.amount.token), existingOwner) + } } else states for (state in gathered) tx.addInputState(state) diff --git a/node/src/test/kotlin/com/r3corda/node/services/NodeSchedulerServiceTest.kt b/node/src/test/kotlin/com/r3corda/node/services/NodeSchedulerServiceTest.kt index 9c6182ae7c..30008eed50 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/NodeSchedulerServiceTest.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/NodeSchedulerServiceTest.kt @@ -1,35 +1,32 @@ package com.r3corda.node.services -import com.google.common.jimfs.Configuration -import com.google.common.jimfs.Jimfs import com.r3corda.core.contracts.* import com.r3corda.core.days import com.r3corda.core.node.ServiceHub import com.r3corda.core.node.recordTransactions -import com.r3corda.core.node.services.VaultService import com.r3corda.core.protocols.ProtocolLogic import com.r3corda.core.protocols.ProtocolLogicRef import com.r3corda.core.protocols.ProtocolLogicRefFactory import com.r3corda.core.serialization.SingletonSerializeAsToken -import com.r3corda.core.transactions.SignedTransaction import com.r3corda.core.utilities.DUMMY_NOTARY import com.r3corda.node.services.events.NodeSchedulerService import com.r3corda.node.services.persistence.DBCheckpointStorage import com.r3corda.node.services.statemachine.StateMachineManager -import com.r3corda.node.services.vault.NodeVaultService import com.r3corda.node.utilities.AddOrRemove import com.r3corda.node.utilities.AffinityExecutor import com.r3corda.node.utilities.configureDatabase import com.r3corda.node.utilities.databaseTransaction import com.r3corda.testing.ALICE_KEY -import com.r3corda.testing.node.* +import com.r3corda.testing.node.InMemoryMessagingNetwork +import com.r3corda.testing.node.MockKeyManagementService +import com.r3corda.testing.node.TestClock +import com.r3corda.testing.node.makeTestDataSourceProperties import org.assertj.core.api.Assertions.assertThat import org.jetbrains.exposed.sql.Database import org.junit.After import org.junit.Before import org.junit.Test import java.io.Closeable -import java.nio.file.FileSystem import java.security.PublicKey import java.time.Clock import java.time.Instant @@ -39,8 +36,6 @@ import java.util.concurrent.TimeUnit import kotlin.test.assertTrue class NodeSchedulerServiceTest : SingletonSerializeAsToken() { - // Use an in memory file system for testing attachment storage. - val fs: FileSystem = Jimfs.newFileSystem(Configuration.unix()) val realClock: Clock = Clock.systemUTC() val stoppedClock = Clock.fixed(realClock.instant(), realClock.zone) @@ -82,19 +77,7 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { dataSource = dataSourceAndDatabase.first database = dataSourceAndDatabase.second - // Switched from InMemoryVault usage to NodeVault databaseTransaction(database) { - val services1 = object : MockServices() { - override val vaultService: VaultService = NodeVaultService(this) - - override fun recordTransactions(txs: Iterable<SignedTransaction>) { - for (stx in txs) { - storageService.validatedTransactions.addTransaction(stx) - vaultService.notify(stx.tx) - } - } - - } val kms = MockKeyManagementService(ALICE_KEY) val mockMessagingService = InMemoryMessagingNetwork(false).InMemoryMessaging(false, InMemoryMessagingNetwork.Handle(0, "None"), AffinityExecutor.ServiceAffinityExecutor("test", 1), database) services = object : MockServiceHubInternal(overrideClock = testClock, keyManagement = kms, net = mockMessagingService), TestReference { diff --git a/src/main/kotlin/com/r3corda/demos/TraderDemo.kt b/src/main/kotlin/com/r3corda/demos/TraderDemo.kt new file mode 100644 index 0000000000..e69de29bb2