From 67b2d91b33c73220140820d729c5d3741895691d Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Mon, 17 Oct 2016 11:16:53 +0100 Subject: [PATCH 1/9] Re-factoring of CashBalances code (moved to VaultService) --- .../com/r3corda/contracts/CommercialPaper.kt | 4 +- .../contracts/CommercialPaperLegacy.kt | 2 +- .../com/r3corda/contracts/asset/Cash.kt | 12 ---- .../r3corda/contracts/asset/FungibleAsset.kt | 67 ------------------- .../clause/AbstractConserveAmount.kt | 8 +-- .../contracts/clause/NoZeroSizedOutputs.kt | 2 +- .../contracts/asset/CashTestsJava.java | 2 +- .../com/r3corda/contracts/asset/CashTests.kt | 4 +- .../contracts/asset/ObligationTests.kt | 4 +- .../com/r3corda/core/contracts/Structures.kt | 64 ++++++++++++++++++ .../r3corda/core/node/services/Services.kt | 7 ++ .../core/testing/InMemoryVaultService.kt | 11 +++ .../com/r3corda/node/internal/ServerRPCOps.kt | 2 +- .../vault/CashBalanceAsMetricsObserver.kt | 7 +- .../node/services/vault/NodeVaultService.kt | 11 +++ .../node/services/VaultWithCashTest.kt | 9 ++- .../kotlin/com/r3corda/demos/TraderDemo.kt | 3 +- 17 files changed, 115 insertions(+), 104 deletions(-) delete mode 100644 contracts/src/main/kotlin/com/r3corda/contracts/asset/FungibleAsset.kt diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt index 48f0105259..f2aae1d7ce 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt @@ -1,8 +1,8 @@ package com.r3corda.contracts import com.r3corda.contracts.asset.Cash -import com.r3corda.contracts.asset.FungibleAsset -import com.r3corda.contracts.asset.InsufficientBalanceException +import com.r3corda.core.contracts.FungibleAsset +import com.r3corda.core.contracts.InsufficientBalanceException import com.r3corda.contracts.asset.sumCashBy import com.r3corda.contracts.clause.AbstractIssue import com.r3corda.core.contracts.* diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaperLegacy.kt b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaperLegacy.kt index a4cc7e2e15..fc7a6c71c6 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaperLegacy.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaperLegacy.kt @@ -1,7 +1,7 @@ package com.r3corda.contracts import com.r3corda.contracts.asset.Cash -import com.r3corda.contracts.asset.InsufficientBalanceException +import com.r3corda.core.contracts.InsufficientBalanceException import com.r3corda.contracts.asset.sumCashBy import com.r3corda.core.contracts.* import com.r3corda.core.crypto.NullPublicKey diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/asset/Cash.kt b/contracts/src/main/kotlin/com/r3corda/contracts/asset/Cash.kt index d4fb1db7e9..423509073a 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/asset/Cash.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/asset/Cash.kt @@ -196,18 +196,6 @@ fun Iterable.sumCashOrZero(currency: Issued): Amount().map { it.amount }.sumOrZero(currency) } -/** - * Returns a map of how much cash we have in each currency, ignoring details like issuer. Note: currencies for - * which we have no cash evaluate to null (not present in map), not 0. - */ -val Vault.cashBalances: Map> get() = states. - // Select the states we own which are cash, ignore the rest, take the amounts. - mapNotNull { (it.state.data as? Cash.State)?.amount }. - // Turn into a Map> like { GBP -> (£100, £500, etc), USD -> ($2000, $50) } - groupBy { it.token.product }. - // Collapse to Map by summing all the amounts of the same currency together. - mapValues { it.value.map { Amount(it.quantity, it.token.product) }.sumOrThrow() } - fun Cash.State.ownedBy(owner: PublicKey) = copy(owner = owner) fun Cash.State.issuedBy(party: Party) = copy(amount = Amount(amount.quantity, issuanceDef.copy(issuer = deposit.copy(party = party)))) fun Cash.State.issuedBy(deposit: PartyAndReference) = copy(amount = Amount(amount.quantity, issuanceDef.copy(issuer = deposit))) diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/asset/FungibleAsset.kt b/contracts/src/main/kotlin/com/r3corda/contracts/asset/FungibleAsset.kt deleted file mode 100644 index f1bed905f6..0000000000 --- a/contracts/src/main/kotlin/com/r3corda/contracts/asset/FungibleAsset.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.r3corda.contracts.asset - -import com.r3corda.core.contracts.* -import java.security.PublicKey - -class InsufficientBalanceException(val amountMissing: Amount<*>) : Exception() { - override fun toString() = "Insufficient balance, missing $amountMissing" -} - -/** - * Interface for contract states representing assets which are fungible, countable and issued by a - * specific party. States contain assets which are equivalent (such as cash of the same currency), - * so records of their existence can be merged or split as needed where the issuer is the same. For - * instance, dollars issued by the Fed are fungible and countable (in cents), barrels of West Texas - * crude are fungible and countable (oil from two small containers can be poured into one large - * container), shares of the same class in a specific company are fungible and countable, and so on. - * - * See [Cash] for an example contract that implements currency using state objects that implement - * this interface. - * - * @param T a type that represents the asset in question. This should describe the basic type of the asset - * (GBP, USD, oil, shares in company , etc.) and any additional metadata (issuer, grade, class, etc.). - */ -interface FungibleAsset : OwnableState { - /** - * Where the underlying asset backing this ledger entry can be found. The reference - * is only intended for use by the issuer, and is not intended to be meaningful to others. - */ - val deposit: PartyAndReference - val issuanceDef: Issued - val amount: Amount> - /** - * There must be an ExitCommand signed by these keys to destroy the amount. While all states require their - * owner to sign, some (i.e. cash) also require the issuer. - */ - val exitKeys: Collection - /** There must be a MoveCommand signed by this key to claim the amount */ - override val owner: PublicKey - fun move(newAmount: Amount>, newOwner: PublicKey): FungibleAsset - - // Just for grouping - interface Commands : CommandData { - interface Move : MoveCommand, Commands - - /** - * Allows new asset states to be issued into existence: the nonce ("number used once") ensures the transaction - * has a unique ID even when there are no inputs. - */ - interface Issue : IssueCommand, Commands - - /** - * A command stating that money has been withdrawn from the shared ledger and is now accounted for - * in some other way. - */ - interface Exit : Commands { val amount: Amount> } - } -} - - -// Small DSL extensions. - -/** Sums the asset states in the list, returning null if there are none. */ -fun Iterable.sumFungibleOrNull() = filterIsInstance>().map { it.amount }.sumOrNull() - -/** Sums the asset states in the list, returning zero of the given token if there are none. */ -fun Iterable.sumFungibleOrZero(token: Issued) = filterIsInstance>().map { it.amount }.sumOrZero(token) - diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractConserveAmount.kt b/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractConserveAmount.kt index 5095f9cbf0..b8e01b2504 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractConserveAmount.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractConserveAmount.kt @@ -1,9 +1,9 @@ package com.r3corda.contracts.clause -import com.r3corda.contracts.asset.FungibleAsset -import com.r3corda.contracts.asset.InsufficientBalanceException -import com.r3corda.contracts.asset.sumFungibleOrNull -import com.r3corda.contracts.asset.sumFungibleOrZero +import com.r3corda.core.contracts.FungibleAsset +import com.r3corda.core.contracts.InsufficientBalanceException +import com.r3corda.core.contracts.sumFungibleOrNull +import com.r3corda.core.contracts.sumFungibleOrZero import com.r3corda.core.contracts.* import com.r3corda.core.contracts.clauses.Clause import com.r3corda.core.crypto.Party diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/clause/NoZeroSizedOutputs.kt b/contracts/src/main/kotlin/com/r3corda/contracts/clause/NoZeroSizedOutputs.kt index f214587c89..366d7b9d30 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/clause/NoZeroSizedOutputs.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/clause/NoZeroSizedOutputs.kt @@ -1,6 +1,6 @@ package com.r3corda.contracts.clause -import com.r3corda.contracts.asset.FungibleAsset +import com.r3corda.core.contracts.FungibleAsset import com.r3corda.core.contracts.* import com.r3corda.core.contracts.clauses.Clause diff --git a/contracts/src/test/java/com/r3corda/contracts/asset/CashTestsJava.java b/contracts/src/test/java/com/r3corda/contracts/asset/CashTestsJava.java index 28bb658bf0..651b5d306f 100644 --- a/contracts/src/test/java/com/r3corda/contracts/asset/CashTestsJava.java +++ b/contracts/src/test/java/com/r3corda/contracts/asset/CashTestsJava.java @@ -33,7 +33,7 @@ public class CashTestsJava { tx.tweak(tw -> { tw.output(outState); // No command arguments - return tw.failsWith("required com.r3corda.contracts.asset.FungibleAsset.Commands.Move command"); + return tw.failsWith("required com.r3corda.core.contracts.FungibleAsset.Commands.Move command"); }); tx.tweak(tw -> { tw.output(outState); 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 e2fbcbaacf..88ad5ac9b8 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt @@ -42,7 +42,7 @@ class CashTests { tweak { output { outState } // No command arguments - this `fails with` "required com.r3corda.contracts.asset.FungibleAsset.Commands.Move command" + this `fails with` "required com.r3corda.core.contracts.FungibleAsset.Commands.Move command" } tweak { output { outState } @@ -312,7 +312,7 @@ class CashTests { tweak { command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer) } - this `fails with` "required com.r3corda.contracts.asset.FungibleAsset.Commands.Move command" + this `fails with` "required com.r3corda.core.contracts.FungibleAsset.Commands.Move command" tweak { command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/asset/ObligationTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/asset/ObligationTests.kt index 0eff89a578..e265a5bbb8 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/asset/ObligationTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/asset/ObligationTests.kt @@ -61,7 +61,7 @@ class ObligationTests { tweak { output { outState } // No command arguments - this `fails with` "required com.r3corda.contracts.asset.FungibleAsset.Commands.Move command" + this `fails with` "required com.r3corda.core.contracts.FungibleAsset.Commands.Move command" } tweak { output { outState } @@ -655,7 +655,7 @@ class ObligationTests { tweak { command(DUMMY_PUBKEY_1) { Obligation.Commands.Exit(Amount(200.DOLLARS.quantity, inState.issuanceDef)) } - this `fails with` "required com.r3corda.contracts.asset.FungibleAsset.Commands.Move command" + this `fails with` "required com.r3corda.core.contracts.FungibleAsset.Commands.Move command" tweak { command(DUMMY_PUBKEY_1) { Obligation.Commands.Move() } diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt b/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt index 56f7f15e11..015d738f36 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt @@ -467,3 +467,67 @@ interface Attachment : NamedByHash { throw FileNotFoundException() } } + +class InsufficientBalanceException(val amountMissing: Amount<*>) : Exception() { + override fun toString() = "Insufficient balance, missing $amountMissing" +} + +/** + * Interface for contract states representing assets which are fungible, countable and issued by a + * specific party. States contain assets which are equivalent (such as cash of the same currency), + * so records of their existence can be merged or split as needed where the issuer is the same. For + * instance, dollars issued by the Fed are fungible and countable (in cents), barrels of West Texas + * crude are fungible and countable (oil from two small containers can be poured into one large + * container), shares of the same class in a specific company are fungible and countable, and so on. + * + * See [Cash] for an example contract that implements currency using state objects that implement + * this interface. + * + * @param T a type that represents the asset in question. This should describe the basic type of the asset + * (GBP, USD, oil, shares in company , etc.) and any additional metadata (issuer, grade, class, etc.). + */ +interface FungibleAsset : OwnableState { + /** + * Where the underlying asset backing this ledger entry can be found. The reference + * is only intended for use by the issuer, and is not intended to be meaningful to others. + */ + val deposit: PartyAndReference + val issuanceDef: Issued + val amount: Amount> + /** + * There must be an ExitCommand signed by these keys to destroy the amount. While all states require their + * owner to sign, some (i.e. cash) also require the issuer. + */ + val exitKeys: Collection + /** There must be a MoveCommand signed by this key to claim the amount */ + override val owner: PublicKey + fun move(newAmount: Amount>, newOwner: PublicKey): FungibleAsset + + // Just for grouping + interface Commands : CommandData { + interface Move : MoveCommand, Commands + + /** + * Allows new asset states to be issued into existence: the nonce ("number used once") ensures the transaction + * has a unique ID even when there are no inputs. + */ + interface Issue : IssueCommand, Commands + + /** + * A command stating that money has been withdrawn from the shared ledger and is now accounted for + * in some other way. + */ + interface Exit : Commands { val amount: Amount> } + } +} + + +// Small DSL extensions. + +/** Sums the asset states in the list, returning null if there are none. */ +fun Iterable.sumFungibleOrNull() = filterIsInstance>().map { it.amount }.sumOrNull() + +/** Sums the asset states in the list, returning zero of the given token if there are none. */ +fun Iterable.sumFungibleOrZero(token: Issued) = filterIsInstance>().map { it.amount }.sumOrZero(token) + + 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 f1939180dd..85d5578d9f 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 @@ -9,6 +9,7 @@ import rx.Observable import java.security.KeyPair import java.security.PrivateKey import java.security.PublicKey +import java.util.* /** * Session ID to use for services listening for the first message in a session (before a @@ -100,6 +101,12 @@ interface VaultService { */ val updates: Observable + /** + * Returns a map of how much cash we have in each currency, ignoring details like issuer. Note: currencies for + * which we have no cash evaluate to null (not present in map), not 0. + */ + val cashBalances: Map> + /** * Atomically get the current vault and a stream of updates. Note that the Observable buffers updates until the * first subscriber is registered so as to avoid racing with early updates. diff --git a/core/src/main/kotlin/com/r3corda/core/testing/InMemoryVaultService.kt b/core/src/main/kotlin/com/r3corda/core/testing/InMemoryVaultService.kt index f17c540f0d..87fb12635d 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/InMemoryVaultService.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/InMemoryVaultService.kt @@ -40,6 +40,16 @@ open class InMemoryVaultService(protected val services: ServiceHub) : SingletonS override val updates: Observable get() = mutex.content._updatesPublisher + @Suppress("UNCHECKED_CAST") + override val cashBalances: Map> + get() = currentVault.states. + // Select the states we own which are cash, ignore the rest, take the amounts. + mapNotNull { (it.state.data as? FungibleAsset)?.amount }. + // Turn into a Map> like { GBP -> (£100, £500, etc), USD -> ($2000, $50) } + groupBy { it.token.product }. + // Collapse to Map by summing all the amounts of the same currency together. + mapValues { it.value.map { Amount(it.quantity, it.token.product) }.sumOrThrow() } + override fun track(): Pair> { return mutex.locked { Pair(vault, updates.bufferUntilSubscribed()) @@ -130,4 +140,5 @@ open class InMemoryVaultService(protected val services: ServiceHub) : SingletonS return Pair(Vault(newStates), change) } + } 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 6c4cfedf4a..d062bf986f 100644 --- a/node/src/main/kotlin/com/r3corda/node/internal/ServerRPCOps.kt +++ b/node/src/main/kotlin/com/r3corda/node/internal/ServerRPCOps.kt @@ -1,7 +1,7 @@ package com.r3corda.node.internal import com.r3corda.contracts.asset.Cash -import com.r3corda.contracts.asset.InsufficientBalanceException +import com.r3corda.core.contracts.InsufficientBalanceException import com.r3corda.core.contracts.* import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.toStringShort diff --git a/node/src/main/kotlin/com/r3corda/node/services/vault/CashBalanceAsMetricsObserver.kt b/node/src/main/kotlin/com/r3corda/node/services/vault/CashBalanceAsMetricsObserver.kt index 62db734f9f..01bcaad092 100644 --- a/node/src/main/kotlin/com/r3corda/node/services/vault/CashBalanceAsMetricsObserver.kt +++ b/node/src/main/kotlin/com/r3corda/node/services/vault/CashBalanceAsMetricsObserver.kt @@ -1,8 +1,7 @@ package com.r3corda.node.services.vault import com.codahale.metrics.Gauge -import com.r3corda.contracts.asset.cashBalances -import com.r3corda.core.node.services.Vault +import com.r3corda.core.node.services.VaultService import com.r3corda.node.services.api.ServiceHubInternal import java.util.* @@ -13,7 +12,7 @@ class CashBalanceAsMetricsObserver(val serviceHubInternal: ServiceHubInternal) { init { // TODO: Need to consider failure scenarios. This needs to run if the TX is successfully recorded serviceHubInternal.vaultService.updates.subscribe { update -> - exportCashBalancesViaMetrics(serviceHubInternal.vaultService.currentVault) + exportCashBalancesViaMetrics(serviceHubInternal.vaultService) } } @@ -24,7 +23,7 @@ class CashBalanceAsMetricsObserver(val serviceHubInternal: ServiceHubInternal) { private val balanceMetrics = HashMap() - private fun exportCashBalancesViaMetrics(vault: Vault) { + private fun exportCashBalancesViaMetrics(vault: VaultService) { // This is just for demo purposes. We probably shouldn't expose balances via JMX in a real node as that might // be commercially sensitive info that the sysadmins aren't even meant to know. // 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 995826cac6..329469c220 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 @@ -20,6 +20,7 @@ import org.jetbrains.exposed.sql.statements.InsertStatement import rx.Observable import rx.subjects.PublishSubject import java.security.PublicKey +import java.util.* /** * Currently, the node vault service is a very simple RDBMS backed implementation. It will change significantly when @@ -80,6 +81,16 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT override val updates: Observable get() = mutex.locked { _updatesPublisher } + @Suppress("UNCHECKED_CAST") + override val cashBalances: Map> + get() = currentVault.states. + // Select the states we own which are cash, ignore the rest, take the amounts. + mapNotNull { (it.state.data as? FungibleAsset)?.amount }. + // Turn into a Map> like { GBP -> (£100, £500, etc), USD -> ($2000, $50) } + groupBy { it.token.product }. + // Collapse to Map by summing all the amounts of the same currency together. + mapValues { it.value.map { Amount(it.quantity, it.token.product) }.sumOrThrow() } + override fun track(): Pair> { return mutex.locked { Pair(Vault(allUnconsumedStates()), _updatesPublisher.bufferUntilSubscribed()) diff --git a/node/src/test/kotlin/com/r3corda/node/services/VaultWithCashTest.kt b/node/src/test/kotlin/com/r3corda/node/services/VaultWithCashTest.kt index 957ab779fb..634a18deee 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/VaultWithCashTest.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/VaultWithCashTest.kt @@ -2,7 +2,6 @@ package com.r3corda.node.services import com.r3corda.contracts.asset.Cash import com.r3corda.contracts.asset.DUMMY_CASH_ISSUER -import com.r3corda.contracts.asset.cashBalances import com.r3corda.contracts.testing.fillWithSomeTestCash import com.r3corda.core.contracts.* import com.r3corda.core.node.recordTransactions @@ -105,14 +104,14 @@ class VaultWithCashTest { signWith(DUMMY_NOTARY_KEY) }.toSignedTransaction() - assertNull(vault.currentVault.cashBalances[USD]) + assertNull(services.vaultService.cashBalances[USD]) services.recordTransactions(usefulTX) - assertEquals(100.DOLLARS, vault.currentVault.cashBalances[USD]) + assertEquals(100.DOLLARS, services.vaultService.cashBalances[USD]) services.recordTransactions(irrelevantTX) - assertEquals(100.DOLLARS, vault.currentVault.cashBalances[USD]) + assertEquals(100.DOLLARS, services.vaultService.cashBalances[USD]) services.recordTransactions(spendTX) - assertEquals(20.DOLLARS, vault.currentVault.cashBalances[USD]) + assertEquals(20.DOLLARS, services.vaultService.cashBalances[USD]) // TODO: Flesh out these tests as needed. } diff --git a/src/main/kotlin/com/r3corda/demos/TraderDemo.kt b/src/main/kotlin/com/r3corda/demos/TraderDemo.kt index c5d63a2014..6fdee2b41c 100644 --- a/src/main/kotlin/com/r3corda/demos/TraderDemo.kt +++ b/src/main/kotlin/com/r3corda/demos/TraderDemo.kt @@ -5,7 +5,6 @@ import com.google.common.net.HostAndPort import com.google.common.util.concurrent.ListenableFuture import com.r3corda.contracts.CommercialPaper import com.r3corda.contracts.asset.DUMMY_CASH_ISSUER -import com.r3corda.contracts.asset.cashBalances import com.r3corda.contracts.testing.fillWithSomeTestCash import com.r3corda.core.contracts.* import com.r3corda.core.crypto.Party @@ -258,7 +257,7 @@ private class TraderDemoProtocolBuyer(val otherSide: Party, } private fun logBalance() { - val balances = serviceHub.vaultService.currentVault.cashBalances.entries.map { "${it.key.currencyCode} ${it.value}" } + val balances = serviceHub.vaultService.cashBalances.entries.map { "${it.key.currencyCode} ${it.value}" } logger.info("Remaining balance: ${balances.joinToString()}") } From 62dfea2a1ab9ee65970482d6668211fef8966cc7 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Mon, 17 Oct 2016 17:58:26 +0100 Subject: [PATCH 2/9] Re-factoring of OnLedgerAsset generateSpend code (moved to VaultService) --- .../protocols/TwoPartyTradeProtocol.kt | 19 +-- .../r3corda/core/node/services/Services.kt | 10 ++ .../core/testing/InMemoryVaultService.kt | 7 ++ .../com/r3corda/node/internal/ServerRPCOps.kt | 15 +-- .../node/services/vault/NodeVaultService.kt | 119 ++++++++++++++++++ .../kotlin/com/r3corda/node/ServerRPCTest.kt | 1 - .../node/services/VaultWithCashTest.kt | 15 +-- .../com/r3corda/testing/node/MockNode.kt | 3 +- 8 files changed, 160 insertions(+), 29 deletions(-) diff --git a/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt b/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt index 5756af476e..8ce6d421b8 100644 --- a/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt +++ b/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt @@ -239,26 +239,27 @@ object TwoPartyTradeProtocol { private fun assembleSharedTX(tradeRequest: SellerTradeInfo): Pair> { val ptx = TransactionType.General.Builder(notary) - // Add input and output states for the movement of cash, by using the Cash contract to generate the states. - val vault = serviceHub.vaultService.currentVault - val cashStates = vault.statesOfType() - val cashSigningPubKeys = Cash().generateSpend(ptx, tradeRequest.price, tradeRequest.sellerOwnerKey, cashStates) + + // 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.sellerOwnerKey) + // Add inputs/outputs/a command for the movement of the asset. - ptx.addInputState(tradeRequest.assetForSale) + tx.addInputState(tradeRequest.assetForSale) + // Just pick some new public key for now. This won't be linked with our identity in any way, which is what // we want for privacy reasons: the key is here ONLY to manage and control ownership, it is not intended to // reveal who the owner actually is. The key management service is expected to derive a unique key from some // initial seed in order to provide privacy protection. val freshKey = serviceHub.keyManagementService.freshKey() val (command, state) = tradeRequest.assetForSale.state.data.withNewOwner(freshKey.public) - ptx.addOutputState(state, tradeRequest.assetForSale.state.notary) - ptx.addCommand(command, tradeRequest.assetForSale.state.data.owner) + tx.addOutputState(state, tradeRequest.assetForSale.state.notary) + tx.addCommand(command, tradeRequest.assetForSale.state.data.owner) // And add a request for timestamping: it may be that none of the contracts need this! But it can't hurt // to have one. val currentTime = serviceHub.clock.instant() - ptx.setTime(currentTime, 30.seconds) - return Pair(ptx, cashSigningPubKeys) + tx.setTime(currentTime, 30.seconds) + return Pair(tx, cashSigningPubKeys) } } } \ No newline at end of file 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 85d5578d9f..b4bd737aba 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 @@ -4,6 +4,7 @@ import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.SettableFuture import com.r3corda.core.contracts.* import com.r3corda.core.crypto.Party +import com.r3corda.core.transactions.TransactionBuilder import com.r3corda.core.transactions.WireTransaction import rx.Observable import java.security.KeyPair @@ -154,6 +155,15 @@ interface VaultService { } return future } + + /** + * Fungible Asset operations + **/ + @Throws(InsufficientBalanceException::class) + fun generateSpend(tx: TransactionBuilder, + amount: Amount, + to: PublicKey, + onlyFromParties: Set? = null): Pair> } inline fun VaultService.linearHeadsOfType() = linearHeadsOfType_(T::class.java) diff --git a/core/src/main/kotlin/com/r3corda/core/testing/InMemoryVaultService.kt b/core/src/main/kotlin/com/r3corda/core/testing/InMemoryVaultService.kt index 87fb12635d..f7f970f1ea 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/InMemoryVaultService.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/InMemoryVaultService.kt @@ -3,10 +3,12 @@ package com.r3corda.core.testing import com.r3corda.core.ThreadBox import com.r3corda.core.bufferUntilSubscribed import com.r3corda.core.contracts.* +import com.r3corda.core.crypto.Party import com.r3corda.core.node.ServiceHub import com.r3corda.core.node.services.Vault import com.r3corda.core.node.services.VaultService import com.r3corda.core.serialization.SingletonSerializeAsToken +import com.r3corda.core.transactions.TransactionBuilder import com.r3corda.core.transactions.WireTransaction import com.r3corda.core.utilities.loggerFor import com.r3corda.core.utilities.trace @@ -104,6 +106,11 @@ open class InMemoryVaultService(protected val services: ServiceHub) : SingletonS return changedVault } + override fun generateSpend(tx: TransactionBuilder, amount: Amount, to: PublicKey, onlyFromParties: Set?): Pair> { + throw UnsupportedOperationException("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + private fun isRelevant(state: ContractState, ourKeys: Set): Boolean { return if (state is OwnableState) { state.owner in ourKeys 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 d062bf986f..2446fe25be 100644 --- a/node/src/main/kotlin/com/r3corda/node/internal/ServerRPCOps.kt +++ b/node/src/main/kotlin/com/r3corda/node/internal/ServerRPCOps.kt @@ -73,21 +73,14 @@ class ServerRPCOps( val builder: TransactionBuilder = TransactionType.General.Builder(null) // TODO: Have some way of restricting this to states the caller controls try { - val vaultCashStates = services.vaultService.currentVault.statesOfType() - // TODO: Move cash state filtering by issuer down to the contract itself - val cashStatesOfRightCurrency = vaultCashStates.filter { it.state.data.amount.token == req.amount.token } - val keysForSigning = Cash().generateSpend( - tx = builder, - amount = req.amount.withoutIssuer(), - to = req.recipient.owningKey, - assetsStates = cashStatesOfRightCurrency, - onlyFromParties = setOf(req.amount.token.issuer.party) - ) + val (spendTX, keysForSigning) = services.vaultService.generateSpend(builder, req.amount.withoutIssuer(), req.recipient.owningKey) + keysForSigning.forEach { val key = services.keyManagementService.keys[it] ?: throw IllegalStateException("Could not find signing key for ${it.toStringShort()}") builder.signWith(KeyPair(it, key)) } - val tx = builder.toSignedTransaction(checkSufficientSignatures = false) + + val tx = spendTX.toSignedTransaction(checkSufficientSignatures = false) val protocol = FinalityProtocol(tx, setOf(req), setOf(req.recipient)) return TransactionBuildResult.ProtocolStarted( smm.add(protocol).id, 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 329469c220..1f0011d749 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 @@ -1,13 +1,16 @@ package com.r3corda.node.services.vault import com.google.common.collect.Sets +import com.r3corda.contracts.asset.Cash import com.r3corda.core.ThreadBox import com.r3corda.core.bufferUntilSubscribed import com.r3corda.core.contracts.* +import com.r3corda.core.crypto.Party import com.r3corda.core.node.ServiceHub import com.r3corda.core.node.services.Vault import com.r3corda.core.node.services.VaultService import com.r3corda.core.serialization.SingletonSerializeAsToken +import com.r3corda.core.transactions.TransactionBuilder import com.r3corda.core.transactions.WireTransaction import com.r3corda.core.utilities.loggerFor import com.r3corda.core.utilities.trace @@ -117,6 +120,121 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT return currentVault } + /** + * 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. + */ + override fun generateSpend(tx: TransactionBuilder, + amount: Amount, + to: PublicKey, + onlyFromParties: Set?): Pair> { + // 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 assetsStates = currentVault.statesOfType() + + 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(Cash().generateMoveCommand(), keysList) + + // update Vault + // notify(tx.toWireTransaction()) + // Vault update must be completed AFTER transaction is recorded to ledger storage!!! + // (this is accomplished within the recordTransaction function) + + return Pair(tx, keysList) + } + + private fun deriveState(txState: TransactionState, amount: Amount>, owner: PublicKey) + = txState.copy(data = txState.data.copy(amount = amount, owner = owner)) + + /** + * Gather assets from the given list of states, sufficient to match or exceed the given amount. + * + * @param acceptableCoins list of states to use as inputs. + * @param amount the amount to gather states up to. + * @throws InsufficientBalanceException if there isn't enough value in the states to cover the requested amount. + */ + @Throws(InsufficientBalanceException::class) + private fun gatherCoins(acceptableCoins: Collection>, + amount: Amount): Pair>, Amount> { + val gathered = arrayListOf>() + var gatheredAmount = Amount(0, amount.token) + for (c in acceptableCoins) { + if (gatheredAmount >= amount) break + gathered.add(c) + gatheredAmount += Amount(c.state.data.amount.quantity, amount.token) + } + + if (gatheredAmount < amount) + throw InsufficientBalanceException(amount - gatheredAmount) + + return Pair(gathered, gatheredAmount) + } + private fun makeUpdate(tx: WireTransaction, netDelta: Vault.Update, ourKeys: Set): Vault.Update { val ourNewStates = tx.outputs. filter { isRelevant(it.data, ourKeys) }. @@ -151,4 +269,5 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT false } } + } diff --git a/node/src/test/kotlin/com/r3corda/node/ServerRPCTest.kt b/node/src/test/kotlin/com/r3corda/node/ServerRPCTest.kt index fc6acf55b7..fcb429deaa 100644 --- a/node/src/test/kotlin/com/r3corda/node/ServerRPCTest.kt +++ b/node/src/test/kotlin/com/r3corda/node/ServerRPCTest.kt @@ -7,7 +7,6 @@ import com.r3corda.core.node.services.Vault import com.r3corda.core.protocols.StateMachineRunId import com.r3corda.core.serialization.OpaqueBytes import com.r3corda.core.transactions.SignedTransaction -import com.r3corda.core.utilities.DUMMY_NOTARY import com.r3corda.node.internal.ServerRPCOps import com.r3corda.node.services.messaging.StateMachineUpdate import com.r3corda.node.services.network.NetworkMapService diff --git a/node/src/test/kotlin/com/r3corda/node/services/VaultWithCashTest.kt b/node/src/test/kotlin/com/r3corda/node/services/VaultWithCashTest.kt index 634a18deee..73e5b8e7b5 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/VaultWithCashTest.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/VaultWithCashTest.kt @@ -88,15 +88,19 @@ class VaultWithCashTest { Cash().generateIssue(this, 100.DOLLARS `issued by` MEGA_CORP.ref(1), freshKey.public, DUMMY_NOTARY) signWith(MEGA_CORP_KEY) }.toSignedTransaction() - val myOutput = usefulTX.toLedgerTransaction(services).outRef(0) + + assertNull(vault.cashBalances[USD]) + services.recordTransactions(usefulTX) // A tx that spends our money. val spendTX = TransactionType.General.Builder(DUMMY_NOTARY).apply { - Cash().generateSpend(this, 80.DOLLARS, BOB_PUBKEY, listOf(myOutput)) + vault.generateSpend(this, 80.DOLLARS, BOB_PUBKEY) signWith(freshKey) signWith(DUMMY_NOTARY_KEY) }.toSignedTransaction() + assertEquals(100.DOLLARS, vault.cashBalances[USD]) + // A tx that doesn't send us anything. val irrelevantTX = TransactionType.General.Builder(DUMMY_NOTARY).apply { Cash().generateIssue(this, 100.DOLLARS `issued by` MEGA_CORP.ref(1), BOB_KEY.public, DUMMY_NOTARY) @@ -104,14 +108,11 @@ class VaultWithCashTest { signWith(DUMMY_NOTARY_KEY) }.toSignedTransaction() - assertNull(services.vaultService.cashBalances[USD]) - services.recordTransactions(usefulTX) - assertEquals(100.DOLLARS, services.vaultService.cashBalances[USD]) services.recordTransactions(irrelevantTX) - assertEquals(100.DOLLARS, services.vaultService.cashBalances[USD]) + assertEquals(100.DOLLARS, vault.cashBalances[USD]) services.recordTransactions(spendTX) - assertEquals(20.DOLLARS, services.vaultService.cashBalances[USD]) + assertEquals(20.DOLLARS, vault.cashBalances[USD]) // TODO: Flesh out these tests as needed. } diff --git a/test-utils/src/main/kotlin/com/r3corda/testing/node/MockNode.kt b/test-utils/src/main/kotlin/com/r3corda/testing/node/MockNode.kt index 537a9c192f..0f32c08a57 100644 --- a/test-utils/src/main/kotlin/com/r3corda/testing/node/MockNode.kt +++ b/test-utils/src/main/kotlin/com/r3corda/testing/node/MockNode.kt @@ -27,6 +27,7 @@ import com.r3corda.node.services.persistence.PerFileCheckpointStorage import com.r3corda.node.services.transactions.InMemoryUniquenessProvider import com.r3corda.node.services.transactions.SimpleNotaryService import com.r3corda.node.services.transactions.ValidatingNotaryService +import com.r3corda.node.services.vault.NodeVaultService import com.r3corda.node.utilities.AffinityExecutor import com.r3corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor import com.r3corda.node.utilities.databaseTransaction @@ -139,7 +140,7 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, override fun makeIdentityService() = MockIdentityService(mockNet.identities) - override fun makeVaultService(): VaultService = InMemoryVaultService(services) + override fun makeVaultService(): VaultService = NodeVaultService(services) override fun makeKeyManagementService(): KeyManagementService = E2ETestKeyManagementService(partyKeys) From c23aea39974e6854e3aa2b8961f8ac1fe1490759 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Tue, 18 Oct 2016 10:27:12 +0100 Subject: [PATCH 3/9] Fixed broken tests caused by missing Transaction Context (when moving from InMemory to Db implementation of vault service in MockNetwork) --- .../kotlin/com/r3corda/node/ServerRPCTest.kt | 5 +++- .../persistence/DataVendingServiceTests.kt | 25 +++++++++++-------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/node/src/test/kotlin/com/r3corda/node/ServerRPCTest.kt b/node/src/test/kotlin/com/r3corda/node/ServerRPCTest.kt index fcb429deaa..d0a40228f6 100644 --- a/node/src/test/kotlin/com/r3corda/node/ServerRPCTest.kt +++ b/node/src/test/kotlin/com/r3corda/node/ServerRPCTest.kt @@ -11,6 +11,7 @@ import com.r3corda.node.internal.ServerRPCOps import com.r3corda.node.services.messaging.StateMachineUpdate import com.r3corda.node.services.network.NetworkMapService import com.r3corda.node.services.transactions.SimpleNotaryService +import com.r3corda.node.utilities.databaseTransaction import com.r3corda.testing.expect import com.r3corda.testing.expectEvents import com.r3corda.testing.node.MockNetwork @@ -53,7 +54,9 @@ class ServerRPCTest { val ref = OpaqueBytes(ByteArray(1) {1}) // Check the monitoring service wallet is empty - assertFalse(aliceNode.services.vaultService.currentVault.states.iterator().hasNext()) + databaseTransaction(aliceNode.database) { + assertFalse(aliceNode.services.vaultService.currentVault.states.iterator().hasNext()) + } // Tell the monitoring service node to issue some cash val recipient = aliceNode.info.legalIdentity diff --git a/node/src/test/kotlin/com/r3corda/node/services/persistence/DataVendingServiceTests.kt b/node/src/test/kotlin/com/r3corda/node/services/persistence/DataVendingServiceTests.kt index 5e03089070..e497be06de 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/persistence/DataVendingServiceTests.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/persistence/DataVendingServiceTests.kt @@ -11,6 +11,7 @@ import com.r3corda.core.protocols.ProtocolLogic import com.r3corda.core.transactions.SignedTransaction import com.r3corda.core.utilities.DUMMY_NOTARY import com.r3corda.node.services.persistence.DataVending.Service.NotifyTransactionHandler +import com.r3corda.node.utilities.databaseTransaction import com.r3corda.protocols.BroadcastTransactionProtocol.NotifyTxRequest import com.r3corda.testing.MEGA_CORP import com.r3corda.testing.node.MockNetwork @@ -45,15 +46,17 @@ class DataVendingServiceTests { val registerKey = registerNode.services.legalIdentityKey ptx.signWith(registerKey) val tx = ptx.toSignedTransaction() - assertEquals(0, vaultServiceNode.services.vaultService.currentVault.states.toList().size) + databaseTransaction(vaultServiceNode.database) { + assertEquals(0, vaultServiceNode.services.vaultService.currentVault.states.toList().size) - registerNode.sendNotifyTx(tx, vaultServiceNode) + registerNode.sendNotifyTx(tx, vaultServiceNode) - // Check the transaction is in the receiving node - val actual = vaultServiceNode.services.vaultService.currentVault.states.singleOrNull() - val expected = tx.tx.outRef(0) + // Check the transaction is in the receiving node + val actual = vaultServiceNode.services.vaultService.currentVault.states.singleOrNull() + val expected = tx.tx.outRef(0) - assertEquals(expected, actual) + assertEquals(expected, actual) + } } /** @@ -74,12 +77,14 @@ class DataVendingServiceTests { val registerKey = registerNode.services.legalIdentityKey ptx.signWith(registerKey) val tx = ptx.toSignedTransaction(false) - assertEquals(0, vaultServiceNode.services.vaultService.currentVault.states.toList().size) + databaseTransaction(vaultServiceNode.database) { + assertEquals(0, vaultServiceNode.services.vaultService.currentVault.states.toList().size) - registerNode.sendNotifyTx(tx, vaultServiceNode) + registerNode.sendNotifyTx(tx, vaultServiceNode) - // Check the transaction is not in the receiving node - assertEquals(0, vaultServiceNode.services.vaultService.currentVault.states.toList().size) + // Check the transaction is not in the receiving node + assertEquals(0, vaultServiceNode.services.vaultService.currentVault.states.toList().size) + } } private fun MockNode.sendNotifyTx(tx: SignedTransaction, walletServiceNode: MockNode) { From ef2ff777a70f40e2dcb9ab7ef03dd1a3023ee4a6 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Wed, 19 Oct 2016 10:38:14 +0100 Subject: [PATCH 4/9] 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) --- .../contracts/JavaCommercialPaper.java | 5 +- .../com/r3corda/contracts/CommercialPaper.kt | 5 +- .../contracts/CommercialPaperLegacy.kt | 6 +- .../r3corda/contracts/asset/OnLedgerAsset.kt | 19 -- .../clause/AbstractConserveAmount.kt | 84 --------- .../r3corda/contracts/testing/VaultFiller.kt | 10 +- .../r3corda/contracts/CommercialPaperTests.kt | 137 ++++++++------ .../com/r3corda/contracts/asset/CashTests.kt | 169 ++++++++++++++---- 8 files changed, 236 insertions(+), 199 deletions(-) diff --git a/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java b/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java index ca850ecf14..d344bd2b9f 100644 --- a/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java +++ b/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java @@ -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 paper, List> 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 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())); } diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt index f2aae1d7ce..f157849159 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt @@ -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, vault: List>) { + fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef, 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) } diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaperLegacy.kt b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaperLegacy.kt index fc7a6c71c6..cc97eb7637 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaperLegacy.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaperLegacy.kt @@ -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, vault: Vault) { + fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef, 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()) + vault.generateSpend(tx, paper.state.data.faceValue.withoutIssuer(), paper.state.data.owner) tx.addInputState(paper) tx.addCommand(Command(Commands.Redeem(), paper.state.data.owner)) } diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/asset/OnLedgerAsset.kt b/contracts/src/main/kotlin/com/r3corda/contracts/asset/OnLedgerAsset.kt index 273df7bbec..9c97b1cc60 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/asset/OnLedgerAsset.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/asset/OnLedgerAsset.kt @@ -47,25 +47,6 @@ abstract class OnLedgerAsset> : 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, - to: PublicKey, - assetsStates: List>, - onlyFromParties: Set? = null): List - = conserveClause.generateSpend(tx, amount, to, assetsStates, onlyFromParties, - deriveState = { state, amount, owner -> deriveState(state, amount, owner) }, - generateMoveCommand = { generateMoveCommand() }) - abstract fun generateExitCommand(amount: Amount>): FungibleAsset.Commands.Exit abstract fun generateIssueCommand(): FungibleAsset.Commands.Issue abstract fun generateMoveCommand(): FungibleAsset.Commands.Move diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractConserveAmount.kt b/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractConserveAmount.kt index b8e01b2504..3313ff4215 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractConserveAmount.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractConserveAmount.kt @@ -85,90 +85,6 @@ abstract class AbstractConserveAmount, 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, - to: PublicKey, - assetsStates: List>, - onlyFromParties: Set? = null, - deriveState: (TransactionState, Amount>, PublicKey) -> TransactionState, - generateMoveCommand: () -> CommandData): List { - // 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, outputs: List, diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/testing/VaultFiller.kt b/contracts/src/main/kotlin/com/r3corda/contracts/testing/VaultFiller.kt index 21e2e2ee4c..3d28dd4e72 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/testing/VaultFiller.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/testing/VaultFiller.kt @@ -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, 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, val cash = Cash() val transactions: List = 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) } diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt index 8c4e3ec3c6..7b135ced29 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt @@ -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,62 +204,90 @@ 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) { - // Propagate the cash transactions to each side. - aliceServices.recordTransactions(bigCorpVault.states.map { bigCorpServices.storageService.validatedTransactions.getTransaction(it.ref.txhash)!! }) - bigCorpServices.recordTransactions(alicesVault.states.map { aliceServices.storageService.validatedTransactions.getTransaction(it.ref.txhash)!! }) + val aliceServices = object : MockServices() { + override val vaultService: VaultService = NodeVaultService(this) - // BigCorp™ issues $10,000 of commercial paper, to mature in 30 days, owned initially by itself. - val faceValue = 10000.DOLLARS `issued by` DUMMY_CASH_ISSUER - val issuance = bigCorpServices.myInfo.legalIdentity.ref(1) - val issueTX: SignedTransaction = - CommercialPaper().generateIssue(issuance, faceValue, TEST_TX_TIME + 30.days, DUMMY_NOTARY).apply { - setTime(TEST_TX_TIME, 30.seconds) - signWith(bigCorpServices.key) - signWith(DUMMY_NOTARY_KEY) - }.toSignedTransaction() + override fun recordTransactions(txs: Iterable) { + 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 - // 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()) - CommercialPaper().generateMove(ptx, issueTX.tx.outRef(0), aliceServices.key.public) - ptx.signWith(bigCorpServices.key) - ptx.signWith(aliceServices.key) - ptx.signWith(DUMMY_NOTARY_KEY) - ptx.toSignedTransaction() + val bigCorpServices = object : MockServices() { + override val vaultService: VaultService = NodeVaultService(this) + + override fun recordTransactions(txs: Iterable) { + 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)!! }) + bigCorpServices.recordTransactions(alicesVault.states.map { aliceServices.storageService.validatedTransactions.getTransaction(it.ref.txhash)!! }) + + // BigCorp™ issues $10,000 of commercial paper, to mature in 30 days, owned initially by itself. + val faceValue = 10000.DOLLARS `issued by` DUMMY_CASH_ISSUER + val issuance = bigCorpServices.myInfo.legalIdentity.ref(1) + val issueTX: SignedTransaction = + CommercialPaper().generateIssue(issuance, faceValue, TEST_TX_TIME + 30.days, DUMMY_NOTARY).apply { + setTime(TEST_TX_TIME, 30.seconds) + signWith(bigCorpServices.key) + signWith(DUMMY_NOTARY_KEY) + }.toSignedTransaction() + + // Alice pays $9000 to BigCorp to own some of their debt. + val moveTX: SignedTransaction = run { + val ptx = TransactionType.General.Builder(DUMMY_NOTARY) + 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) + ptx.signWith(DUMMY_NOTARY_KEY) + ptx.toSignedTransaction() + } + + fun makeRedeemTX(time: Instant): SignedTransaction { + val ptx = TransactionType.General.Builder(DUMMY_NOTARY) + ptx.setTime(time, 30.seconds) + CommercialPaper().generateRedeem(ptx, moveTX.tx.outRef(1), bigCorpVaultService) + ptx.signWith(aliceServices.key) + ptx.signWith(bigCorpServices.key) + ptx.signWith(DUMMY_NOTARY_KEY) + return ptx.toSignedTransaction() + } + + val tooEarlyRedemption = makeRedeemTX(TEST_TX_TIME + 10.days) + val validRedemption = makeRedeemTX(TEST_TX_TIME + 31.days) + + // Verify the txns are valid and insert into both sides. + listOf(issueTX, moveTX).forEach { + it.toLedgerTransaction(aliceServices).verify() + aliceServices.recordTransactions(it) + bigCorpServices.recordTransactions(it) + } + + val e = assertFailsWith(TransactionVerificationException::class) { + tooEarlyRedemption.toLedgerTransaction(aliceServices).verify() + } + assertTrue(e.cause!!.message!!.contains("paper must have matured")) + + validRedemption.toLedgerTransaction(aliceServices).verify() } - - 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()) - ptx.signWith(aliceServices.key) - ptx.signWith(bigCorpServices.key) - ptx.signWith(DUMMY_NOTARY_KEY) - return ptx.toSignedTransaction() - } - - val tooEarlyRedemption = makeRedeemTX(TEST_TX_TIME + 10.days) - val validRedemption = makeRedeemTX(TEST_TX_TIME + 31.days) - - // Verify the txns are valid and insert into both sides. - listOf(issueTX, moveTX).forEach { - it.toLedgerTransaction(aliceServices).verify() - aliceServices.recordTransactions(it) - bigCorpServices.recordTransactions(it) - } - - val e = assertFailsWith(TransactionVerificationException::class) { - tooEarlyRedemption.toLedgerTransaction(aliceServices).verify() - } - assertTrue(e.cause!!.message!!.contains("paper must have matured")) - - validRedemption.toLedgerTransaction(aliceServices).verify() } } 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 88ad5ac9b8..9c7d35f9b9 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt @@ -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) { + 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, corp: Party, depositRef: Byte = 1) = @@ -428,8 +493,10 @@ class CashTests { } fun makeSpend(amount: Amount, 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,58 +552,92 @@ class CashTests { @Test fun generateSimpleDirectSpend() { - 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) - assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0]) + + databaseTransaction(database) { + + val wtx = makeSpend(100.DOLLARS, THEIR_PUBKEY_1) + + val vaultState = VAULT.states.elementAt(0) as StateAndRef + 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() { - 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]) + + databaseTransaction(database) { + + val tx = TransactionType.General.Builder(DUMMY_NOTARY) + vault.generateSpend(tx, 80.DOLLARS, ALICE_PUBKEY, setOf(MINI_CORP)) + + assertEquals(VAULT.states.elementAt(2).ref, tx.inputStates()[0]) + } } @Test fun generateSimpleSpendWithChange() { - 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) - assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0]) + + databaseTransaction(database) { + + val wtx = makeSpend(10.DOLLARS, THEIR_PUBKEY_1) + + val vaultState = VAULT.states.elementAt(0) as StateAndRef + 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() { - 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) - assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0]) + + databaseTransaction(database) { + val wtx = makeSpend(500.DOLLARS, THEIR_PUBKEY_1) + + val vaultState0 = VAULT.states.elementAt(0) as StateAndRef + 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() { - 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) - assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0]) + + databaseTransaction(database) { + val wtx = makeSpend(580.DOLLARS, THEIR_PUBKEY_1) + assertEquals(3, wtx.inputs.size) + + val vaultState0 = VAULT.states.elementAt(0) as StateAndRef + val vaultState1 = VAULT.states.elementAt(1) + val vaultState2 = VAULT.states.elementAt(2) as StateAndRef + 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() { - val e: InsufficientBalanceException = assertFailsWith("balance") { - makeSpend(1000.DOLLARS, THEIR_PUBKEY_1) - } - assertEquals((1000 - 580).DOLLARS, e.amountMissing) - assertFailsWith(InsufficientBalanceException::class) { - makeSpend(81.SWISS_FRANCS, THEIR_PUBKEY_1) + databaseTransaction(database) { + + val e: InsufficientBalanceException = assertFailsWith("balance") { + makeSpend(1000.DOLLARS, THEIR_PUBKEY_1) + } + assertEquals((1000 - 580).DOLLARS, e.amountMissing) + + assertFailsWith(InsufficientBalanceException::class) { + makeSpend(81.SWISS_FRANCS, THEIR_PUBKEY_1) + } } } From c7d98b8c6b5385c0576432f5b6879fa98b038c61 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Thu, 20 Oct 2016 10:52:51 +0100 Subject: [PATCH 5/9] Fixed items raised by MH in CRD-CR-58 code review. --- .../com/r3corda/contracts/asset/CashTests.kt | 21 +++++++++---------- .../r3corda/core/node/services/Services.kt | 8 +++++++ .../core/testing/InMemoryVaultService.kt | 13 ++---------- .../node/services/vault/NodeVaultService.kt | 10 --------- 4 files changed, 20 insertions(+), 32 deletions(-) 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 9c7d35f9b9..7a9ca1c085 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt @@ -50,7 +50,7 @@ class CashTests { val vault: VaultService get() = services.vaultService lateinit var dataSource: Closeable lateinit var database: Database - lateinit var VAULT: Vault + lateinit var vaultService: Vault @Before fun setUp() { @@ -60,7 +60,6 @@ class CashTests { 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) @@ -82,7 +81,7 @@ class CashTests { 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 + vaultService = services.vaultService.currentVault } } @@ -557,7 +556,7 @@ class CashTests { val wtx = makeSpend(100.DOLLARS, THEIR_PUBKEY_1) - val vaultState = VAULT.states.elementAt(0) as StateAndRef + val vaultState = vaultService.states.elementAt(0) as StateAndRef 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]) @@ -572,7 +571,7 @@ class CashTests { val tx = TransactionType.General.Builder(DUMMY_NOTARY) vault.generateSpend(tx, 80.DOLLARS, ALICE_PUBKEY, setOf(MINI_CORP)) - assertEquals(VAULT.states.elementAt(2).ref, tx.inputStates()[0]) + assertEquals(vaultService.states.elementAt(2).ref, tx.inputStates()[0]) } } @@ -583,7 +582,7 @@ class CashTests { val wtx = makeSpend(10.DOLLARS, THEIR_PUBKEY_1) - val vaultState = VAULT.states.elementAt(0) as StateAndRef + val vaultState = vaultService.states.elementAt(0) as StateAndRef 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) @@ -597,8 +596,8 @@ class CashTests { databaseTransaction(database) { val wtx = makeSpend(500.DOLLARS, THEIR_PUBKEY_1) - val vaultState0 = VAULT.states.elementAt(0) as StateAndRef - val vaultState1 = VAULT.states.elementAt(1) + val vaultState0 = vaultService.states.elementAt(0) as StateAndRef + val vaultState1 = vaultService.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) @@ -613,9 +612,9 @@ class CashTests { val wtx = makeSpend(580.DOLLARS, THEIR_PUBKEY_1) assertEquals(3, wtx.inputs.size) - val vaultState0 = VAULT.states.elementAt(0) as StateAndRef - val vaultState1 = VAULT.states.elementAt(1) - val vaultState2 = VAULT.states.elementAt(2) as StateAndRef + val vaultState0 = vaultService.states.elementAt(0) as StateAndRef + val vaultState1 = vaultService.states.elementAt(1) + val vaultState2 = vaultService.states.elementAt(2) as StateAndRef assertEquals(vaultState0.ref, wtx.inputs[0]) assertEquals(vaultState1.ref, wtx.inputs[1]) assertEquals(vaultState2.ref, wtx.inputs[2]) 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 b4bd737aba..322517c859 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 @@ -106,7 +106,15 @@ interface VaultService { * Returns a map of how much cash we have in each currency, ignoring details like issuer. Note: currencies for * which we have no cash evaluate to null (not present in map), not 0. */ + @Suppress("UNCHECKED_CAST") val cashBalances: Map> + get() = currentVault.states. + // Select the states we own which are cash, ignore the rest, take the amounts. + mapNotNull { (it.state.data as? FungibleAsset)?.amount }. + // Turn into a Map> like { GBP -> (£100, £500, etc), USD -> ($2000, $50) } + groupBy { it.token.product }. + // Collapse to Map by summing all the amounts of the same currency together. + mapValues { it.value.map { Amount(it.quantity, it.token.product) }.sumOrThrow() } /** * Atomically get the current vault and a stream of updates. Note that the Observable buffers updates until the diff --git a/core/src/main/kotlin/com/r3corda/core/testing/InMemoryVaultService.kt b/core/src/main/kotlin/com/r3corda/core/testing/InMemoryVaultService.kt index f7f970f1ea..2f8d84441c 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/InMemoryVaultService.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/InMemoryVaultService.kt @@ -42,16 +42,6 @@ open class InMemoryVaultService(protected val services: ServiceHub) : SingletonS override val updates: Observable get() = mutex.content._updatesPublisher - @Suppress("UNCHECKED_CAST") - override val cashBalances: Map> - get() = currentVault.states. - // Select the states we own which are cash, ignore the rest, take the amounts. - mapNotNull { (it.state.data as? FungibleAsset)?.amount }. - // Turn into a Map> like { GBP -> (£100, £500, etc), USD -> ($2000, $50) } - groupBy { it.token.product }. - // Collapse to Map by summing all the amounts of the same currency together. - mapValues { it.value.map { Amount(it.quantity, it.token.product) }.sumOrThrow() } - override fun track(): Pair> { return mutex.locked { Pair(vault, updates.bufferUntilSubscribed()) @@ -107,7 +97,8 @@ open class InMemoryVaultService(protected val services: ServiceHub) : SingletonS } override fun generateSpend(tx: TransactionBuilder, amount: Amount, to: PublicKey, onlyFromParties: Set?): Pair> { - throw UnsupportedOperationException("not implemented") //To change body of created functions use File | Settings | File Templates. + // TODO: decommission entirely this Vault implementation + throw UnsupportedOperationException("Should be using NodeVaultService implementation!") } 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 1f0011d749..2e50747e49 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 @@ -84,16 +84,6 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT override val updates: Observable get() = mutex.locked { _updatesPublisher } - @Suppress("UNCHECKED_CAST") - override val cashBalances: Map> - get() = currentVault.states. - // Select the states we own which are cash, ignore the rest, take the amounts. - mapNotNull { (it.state.data as? FungibleAsset)?.amount }. - // Turn into a Map> like { GBP -> (£100, £500, etc), USD -> ($2000, $50) } - groupBy { it.token.product }. - // Collapse to Map by summing all the amounts of the same currency together. - mapValues { it.value.map { Amount(it.quantity, it.token.product) }.sumOrThrow() } - override fun track(): Pair> { return mutex.locked { Pair(Vault(allUnconsumedStates()), _updatesPublisher.bufferUntilSubscribed()) From 75f671a446a5348251af6d9f8aff647e95d660fc Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Thu, 20 Oct 2016 13:10:00 +0100 Subject: [PATCH 6/9] Decommissioned InMemoryVaultService service (all dependent Tests updated to use NodeVaultService) --- .../core/testing/InMemoryVaultService.kt | 142 ------------------ .../node/services/MockServiceHubInternal.kt | 4 +- .../node/services/NodeSchedulerServiceTest.kt | 74 +++++---- .../com/r3corda/testing/node/MockNode.kt | 1 - 4 files changed, 49 insertions(+), 172 deletions(-) delete mode 100644 core/src/main/kotlin/com/r3corda/core/testing/InMemoryVaultService.kt diff --git a/core/src/main/kotlin/com/r3corda/core/testing/InMemoryVaultService.kt b/core/src/main/kotlin/com/r3corda/core/testing/InMemoryVaultService.kt deleted file mode 100644 index 2f8d84441c..0000000000 --- a/core/src/main/kotlin/com/r3corda/core/testing/InMemoryVaultService.kt +++ /dev/null @@ -1,142 +0,0 @@ -package com.r3corda.core.testing - -import com.r3corda.core.ThreadBox -import com.r3corda.core.bufferUntilSubscribed -import com.r3corda.core.contracts.* -import com.r3corda.core.crypto.Party -import com.r3corda.core.node.ServiceHub -import com.r3corda.core.node.services.Vault -import com.r3corda.core.node.services.VaultService -import com.r3corda.core.serialization.SingletonSerializeAsToken -import com.r3corda.core.transactions.TransactionBuilder -import com.r3corda.core.transactions.WireTransaction -import com.r3corda.core.utilities.loggerFor -import com.r3corda.core.utilities.trace -import rx.Observable -import rx.subjects.PublishSubject -import java.security.PublicKey -import java.util.* -import javax.annotation.concurrent.ThreadSafe - -/** - * This class implements a simple, in memory vault that tracks states that are owned by us, and also has a convenience - * method to auto-generate some self-issued cash states that can be used for test trading. A real vault would persist - * states relevant to us into a database and once such a vault is implemented, this scaffolding can be removed. - */ -@ThreadSafe -open class InMemoryVaultService(protected val services: ServiceHub) : SingletonSerializeAsToken(), VaultService { - open protected val log = loggerFor() - - // Variables inside InnerState are protected with a lock by the ThreadBox and aren't in scope unless you're - // inside mutex.locked {} code block. So we can't forget to take the lock unless we accidentally leak a reference - // to vault somewhere. - protected class InnerState { - var vault = Vault(emptyList>()) - val _updatesPublisher = PublishSubject.create() - } - - protected val mutex = ThreadBox(InnerState()) - - override val currentVault: Vault get() = mutex.locked { vault } - - override val updates: Observable - get() = mutex.content._updatesPublisher - - override fun track(): Pair> { - return mutex.locked { - Pair(vault, updates.bufferUntilSubscribed()) - } - } - - /** - * Returns a snapshot of the heads of LinearStates. - */ - override val linearHeads: Map> - get() = currentVault.let { vault -> - vault.states.filterStatesOfType().associateBy { it.state.data.linearId }.mapValues { it.value } - } - - override fun notifyAll(txns: Iterable): Vault { - val ourKeys = services.keyManagementService.keys.keys - - // Note how terribly incomplete this all is! - // - // - We don't notify anyone of anything, there are no event listeners. - // - We don't handle or even notice invalidations due to double spends of things in our vault. - // - We have no concept of confidence (for txns where there is no definite finality). - // - No notification that keys are used, for the case where we observe a spend of our own states. - // - No ability to create complex spends. - // - No logging or tracking of how the vault got into this state. - // - No persistence. - // - Does tx relevancy calculation and key management need to be interlocked? Probably yes. - // - // ... and many other things .... (Wallet.java in bitcoinj is several thousand lines long) - - var netDelta = Vault.NoUpdate - val changedVault = mutex.locked { - // Starting from the current vault, keep applying the transaction updates, calculating a new vault each - // time, until we get to the result (this is perhaps a bit inefficient, but it's functional and easily - // unit tested). - val vaultAndNetDelta = txns.fold(Pair(currentVault, Vault.NoUpdate)) { vaultAndDelta, tx -> - val (vault, delta) = vaultAndDelta.first.update(tx, ourKeys) - val combinedDelta = delta + vaultAndDelta.second - Pair(vault, combinedDelta) - } - - vault = vaultAndNetDelta.first - netDelta = vaultAndNetDelta.second - return@locked vault - } - - if (netDelta != Vault.NoUpdate) { - mutex.locked { - _updatesPublisher.onNext(netDelta) - } - } - return changedVault - } - - override fun generateSpend(tx: TransactionBuilder, amount: Amount, to: PublicKey, onlyFromParties: Set?): Pair> { - // TODO: decommission entirely this Vault implementation - throw UnsupportedOperationException("Should be using NodeVaultService implementation!") - } - - - private fun isRelevant(state: ContractState, ourKeys: Set): Boolean { - return if (state is OwnableState) { - state.owner in ourKeys - } else if (state is LinearState) { - // It's potentially of interest to the vault - state.isRelevant(ourKeys) - } else { - false - } - } - - private fun Vault.update(tx: WireTransaction, ourKeys: Set): Pair { - val ourNewStates = tx.outputs. - filter { isRelevant(it.data, ourKeys) }. - map { tx.outRef(it.data) } - - // Now calculate the states that are being spent by this transaction. - val consumed: Set = states.map { it.ref }.intersect(tx.inputs) - - // Is transaction irrelevant? - if (consumed.isEmpty() && ourNewStates.isEmpty()) { - log.trace { "tx ${tx.id} was irrelevant to this vault, ignoring" } - return Pair(this, Vault.NoUpdate) - } - - val change = Vault.Update(consumed, HashSet(ourNewStates)) - - // And calculate the new vault. - val newStates = states.filter { it.ref !in consumed } + ourNewStates - - log.trace { - "Applied tx ${tx.id.prefixChars()} to the vault: consumed ${consumed.size} states and added ${newStates.size}" - } - - return Pair(Vault(newStates), change) - } - -} diff --git a/node/src/test/kotlin/com/r3corda/node/services/MockServiceHubInternal.kt b/node/src/test/kotlin/com/r3corda/node/services/MockServiceHubInternal.kt index 24dcf112a7..7195123744 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/MockServiceHubInternal.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/MockServiceHubInternal.kt @@ -7,7 +7,6 @@ import com.r3corda.core.node.NodeInfo import com.r3corda.core.node.services.* import com.r3corda.core.protocols.ProtocolLogic import com.r3corda.core.protocols.ProtocolLogicRefFactory -import com.r3corda.core.testing.InMemoryVaultService import com.r3corda.core.transactions.SignedTransaction import com.r3corda.node.serialization.NodeClock import com.r3corda.node.services.api.MessagingServiceInternal @@ -17,6 +16,7 @@ import com.r3corda.node.services.api.ServiceHubInternal import com.r3corda.node.services.persistence.DataVending import com.r3corda.node.services.schema.NodeSchemaService import com.r3corda.node.services.statemachine.StateMachineManager +import com.r3corda.node.services.vault.NodeVaultService import com.r3corda.testing.MOCK_IDENTITY_SERVICE import com.r3corda.testing.node.MockNetworkMapCache import com.r3corda.testing.node.MockStorageService @@ -37,7 +37,7 @@ open class MockServiceHubInternal( val protocolFactory: ProtocolLogicRefFactory? = ProtocolLogicRefFactory(), val schemas: SchemaService? = NodeSchemaService() ) : ServiceHubInternal() { - override val vaultService: VaultService = customVault ?: InMemoryVaultService(this) + override val vaultService: VaultService = customVault ?: NodeVaultService(this) override val keyManagementService: KeyManagementService get() = keyManagement ?: throw UnsupportedOperationException() override val identityService: IdentityService 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 ed101d64cb..ea7831a765 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/NodeSchedulerServiceTest.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/NodeSchedulerServiceTest.kt @@ -6,23 +6,25 @@ 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.PerFileCheckpointStorage 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.InMemoryMessagingNetwork -import com.r3corda.testing.node.MockKeyManagementService -import com.r3corda.testing.node.TestClock -import com.r3corda.testing.node.makeTestDataSourceProperties +import com.r3corda.testing.node.* 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 @@ -50,11 +52,12 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") val factory = ProtocolLogicRefFactory(mapOf(Pair(TestProtocolLogic::class.java.name, setOf(NodeSchedulerServiceTest::class.java.name, Integer::class.java.name)))) - val services: MockServiceHubInternal + lateinit var services: MockServiceHubInternal lateinit var scheduler: NodeSchedulerService lateinit var smmExecutor: AffinityExecutor.ServiceAffinityExecutor lateinit var dataSource: Closeable + lateinit var database: Database lateinit var countDown: CountDownLatch lateinit var smmHasRemovedAllProtocols: CountDownLatch @@ -69,14 +72,6 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { val testReference: NodeSchedulerServiceTest } - init { - val kms = MockKeyManagementService(ALICE_KEY) - val mockMessagingService = InMemoryMessagingNetwork(false).InMemoryMessaging(false, InMemoryMessagingNetwork.Handle(0, "None"), AffinityExecutor.ServiceAffinityExecutor("test", 1), persistenceTx = { it() }) - services = object : MockServiceHubInternal(overrideClock = testClock, keyManagement = kms, net = mockMessagingService), TestReference { - override val testReference = this@NodeSchedulerServiceTest - } - } - @Before fun setup() { countDown = CountDownLatch(1) @@ -84,7 +79,28 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { calls = 0 val dataSourceAndDatabase = configureDatabase(makeTestDataSourceProperties()) dataSource = dataSourceAndDatabase.first - val database = dataSourceAndDatabase.second + 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) { + 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), persistenceTx = { it() }) + services = object : MockServiceHubInternal(customVault = services1.vaultService, overrideClock = testClock, keyManagement = kms, net = mockMessagingService), TestReference { + override val testReference = this@NodeSchedulerServiceTest + } + } + scheduler = NodeSchedulerService(database, services, factory, schedulerGatedExecutor) smmExecutor = AffinityExecutor.ServiceAffinityExecutor("test", 1) val mockSMM = StateMachineManager(services, listOf(services), PerFileCheckpointStorage(fs.getPath("checkpoints")), smmExecutor, database) @@ -257,20 +273,24 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { } private fun scheduleTX(instant: Instant, increment: Int = 1): ScheduledStateRef? { - var scheduledRef: ScheduledStateRef? = null - apply { - val freshKey = services.keyManagementService.freshKey() - val state = TestState(factory.create(TestProtocolLogic::class.java, increment), instant) - val usefulTX = TransactionType.General.Builder(null).apply { - addOutputState(state, DUMMY_NOTARY) - addCommand(Command(), freshKey.public) - signWith(freshKey) - }.toSignedTransaction() - val txHash = usefulTX.id - services.recordTransactions(usefulTX) - scheduledRef = ScheduledStateRef(StateRef(txHash, 0), state.instant) - scheduler.scheduleStateActivity(scheduledRef!!) + var scheduledRef: ScheduledStateRef? = null + + databaseTransaction(database) { + apply { + val freshKey = services.keyManagementService.freshKey() + val state = TestState(factory.create(TestProtocolLogic::class.java, increment), instant) + val usefulTX = TransactionType.General.Builder(null).apply { + addOutputState(state, DUMMY_NOTARY) + addCommand(Command(), freshKey.public) + signWith(freshKey) + }.toSignedTransaction() + val txHash = usefulTX.id + + services.recordTransactions(usefulTX) + scheduledRef = ScheduledStateRef(StateRef(txHash, 0), state.instant) + scheduler.scheduleStateActivity(scheduledRef!!) + } } return scheduledRef } diff --git a/test-utils/src/main/kotlin/com/r3corda/testing/node/MockNode.kt b/test-utils/src/main/kotlin/com/r3corda/testing/node/MockNode.kt index 0f32c08a57..bb9cc5bd2b 100644 --- a/test-utils/src/main/kotlin/com/r3corda/testing/node/MockNode.kt +++ b/test-utils/src/main/kotlin/com/r3corda/testing/node/MockNode.kt @@ -11,7 +11,6 @@ import com.r3corda.core.node.services.KeyManagementService import com.r3corda.core.node.services.ServiceInfo import com.r3corda.core.node.services.VaultService import com.r3corda.core.random63BitValue -import com.r3corda.core.testing.InMemoryVaultService import com.r3corda.core.utilities.DUMMY_NOTARY_KEY import com.r3corda.core.utilities.loggerFor import com.r3corda.node.internal.AbstractNode From 74e89181da90fcaacc07ac7bce0c376e9f883273 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Thu, 27 Oct 2016 12:57:37 +0100 Subject: [PATCH 7/9] Merge remote-tracking branch 'remotes/origin/master' into colljos-vault-code-clean-up-refactor # Conflicts: # core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt # node/src/test/kotlin/com/r3corda/node/services/NodeSchedulerServiceTest.kt Fixed conflict. --- .../node/services/NodeSchedulerServiceTest.kt | 40 ++++++++----------- 1 file changed, 16 insertions(+), 24 deletions(-) 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 0324e7412a..9c6182ae7c 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/NodeSchedulerServiceTest.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/NodeSchedulerServiceTest.kt @@ -72,13 +72,6 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { val testReference: NodeSchedulerServiceTest } - init { - val kms = MockKeyManagementService(ALICE_KEY) - val mockMessagingService = InMemoryMessagingNetwork(false).InMemoryMessaging(false, InMemoryMessagingNetwork.Handle(0, "None"), AffinityExecutor.ServiceAffinityExecutor("test", 1), persistenceTx = { it() }) - services = object : MockServiceHubInternal(overrideClock = testClock, keyManagement = kms, net = mockMessagingService), TestReference { - override val testReference = this@NodeSchedulerServiceTest - } - } @Before fun setup() { @@ -103,22 +96,22 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { } val kms = MockKeyManagementService(ALICE_KEY) - val mockMessagingService = InMemoryMessagingNetwork(false).InMemoryMessaging(false, InMemoryMessagingNetwork.Handle(0, "None"), AffinityExecutor.ServiceAffinityExecutor("test", 1), persistenceTx = { it() }) - services = object : MockServiceHubInternal(customVault = services1.vaultService, overrideClock = testClock, keyManagement = kms, net = mockMessagingService), TestReference { + 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 { override val testReference = this@NodeSchedulerServiceTest } - } - - scheduler = NodeSchedulerService(database, services, factory, schedulerGatedExecutor) - smmExecutor = AffinityExecutor.ServiceAffinityExecutor("test", 1) - val mockSMM = StateMachineManager(services, listOf(services), PerFileCheckpointStorage(fs.getPath("checkpoints")), smmExecutor, database) - mockSMM.changes.subscribe { change -> - if (change.addOrRemove == AddOrRemove.REMOVE && mockSMM.allStateMachines.isEmpty()) { - smmHasRemovedAllProtocols.countDown() + scheduler = NodeSchedulerService(database, services, factory, schedulerGatedExecutor) + smmExecutor = AffinityExecutor.ServiceAffinityExecutor("test", 1) + val mockSMM = StateMachineManager(services, listOf(services, scheduler), DBCheckpointStorage(), smmExecutor, database) + mockSMM.changes.subscribe { change -> + if (change.addOrRemove == AddOrRemove.REMOVE && mockSMM.allStateMachines.isEmpty()) { + smmHasRemovedAllProtocols.countDown() + } } + mockSMM.start() + services.smm = mockSMM + scheduler.start() } - mockSMM.start() - services.smm = mockSMM } @After @@ -285,9 +278,7 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { } private fun scheduleTX(instant: Instant, increment: Int = 1): ScheduledStateRef? { - var scheduledRef: ScheduledStateRef? = null - databaseTransaction(database) { apply { val freshKey = services.keyManagementService.freshKey() @@ -299,9 +290,10 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { }.toSignedTransaction() val txHash = usefulTX.id - services.recordTransactions(usefulTX) - scheduledRef = ScheduledStateRef(StateRef(txHash, 0), state.instant) - scheduler.scheduleStateActivity(scheduledRef!!) + services.recordTransactions(usefulTX) + scheduledRef = ScheduledStateRef(StateRef(txHash, 0), state.instant) + scheduler.scheduleStateActivity(scheduledRef!!) + } } return scheduledRef } From f94c9f82394b9f1b53e97533d8c59cd5525d8094 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Thu, 27 Oct 2016 12:58:13 +0100 Subject: [PATCH 8/9] Fixed failing Commercial Paper test. --- .../r3corda/contracts/CommercialPaperTests.kt | 69 +++++++++++++------ 1 file changed, 47 insertions(+), 22 deletions(-) diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt index 7b135ced29..0e004b9f2a 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt @@ -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.Vault import com.r3corda.core.node.services.VaultService import com.r3corda.core.seconds import com.r3corda.core.transactions.LedgerTransaction @@ -202,14 +203,29 @@ class CommercialPaperTestsGeneric { return Pair(ltx, outputs.mapIndexed { index, state -> StateAndRef(state, StateRef(ltx.id, index)) }) } + /** + * Unit test requires two separate Database instances to represent each of the two + * transaction participants (enforces uniqueness of vault content in lieu of partipant identity) + */ + + private lateinit var bigCorpServices: MockServices + private lateinit var bigCorpVault: Vault + private lateinit var bigCorpVaultService: VaultService + + private lateinit var aliceServices: MockServices + private lateinit var aliceVaultService: VaultService + private lateinit var alicesVault: Vault + + private lateinit var moveTX: SignedTransaction + @Test fun `issue move and then redeem`() { - val dataSourceAndDatabase = configureDatabase(makeTestDataSourceProperties()) - val database = dataSourceAndDatabase.second - databaseTransaction(database) { + val dataSourceAndDatabaseAlice = configureDatabase(makeTestDataSourceProperties()) + val databaseAlice = dataSourceAndDatabaseAlice.second + databaseTransaction(databaseAlice) { - val aliceServices = object : MockServices() { + aliceServices = object : MockServices() { override val vaultService: VaultService = NodeVaultService(this) override fun recordTransactions(txs: Iterable) { @@ -220,10 +236,15 @@ class CommercialPaperTestsGeneric { vaultService.notifyAll(txs.map { it.tx }) } } - val alicesVault = aliceServices.fillWithSomeTestCash(9000.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1) - val aliceVaultService = aliceServices.vaultService + alicesVault = aliceServices.fillWithSomeTestCash(9000.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1) + aliceVaultService = aliceServices.vaultService + } - val bigCorpServices = object : MockServices() { + val dataSourceAndDatabaseBigCorp = configureDatabase(makeTestDataSourceProperties()) + val databaseBigCorp = dataSourceAndDatabaseBigCorp.second + databaseTransaction(databaseBigCorp) { + + bigCorpServices = object : MockServices() { override val vaultService: VaultService = NodeVaultService(this) override fun recordTransactions(txs: Iterable) { @@ -234,25 +255,27 @@ class CommercialPaperTestsGeneric { vaultService.notifyAll(txs.map { it.tx }) } } - val bigCorpVault = bigCorpServices.fillWithSomeTestCash(13000.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1) - val bigCorpVaultService = bigCorpServices.vaultService + bigCorpVault = bigCorpServices.fillWithSomeTestCash(13000.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1) + bigCorpVaultService = bigCorpServices.vaultService + } - // Propagate the cash transactions to each side. - aliceServices.recordTransactions(bigCorpVault.states.map { bigCorpServices.storageService.validatedTransactions.getTransaction(it.ref.txhash)!! }) - bigCorpServices.recordTransactions(alicesVault.states.map { aliceServices.storageService.validatedTransactions.getTransaction(it.ref.txhash)!! }) + // Propagate the cash transactions to each side. + aliceServices.recordTransactions(bigCorpVault.states.map { bigCorpServices.storageService.validatedTransactions.getTransaction(it.ref.txhash)!! }) + bigCorpServices.recordTransactions(alicesVault.states.map { aliceServices.storageService.validatedTransactions.getTransaction(it.ref.txhash)!! }) - // BigCorp™ issues $10,000 of commercial paper, to mature in 30 days, owned initially by itself. - val faceValue = 10000.DOLLARS `issued by` DUMMY_CASH_ISSUER - val issuance = bigCorpServices.myInfo.legalIdentity.ref(1) - val issueTX: SignedTransaction = - CommercialPaper().generateIssue(issuance, faceValue, TEST_TX_TIME + 30.days, DUMMY_NOTARY).apply { - setTime(TEST_TX_TIME, 30.seconds) - signWith(bigCorpServices.key) - signWith(DUMMY_NOTARY_KEY) - }.toSignedTransaction() + // BigCorp™ issues $10,000 of commercial paper, to mature in 30 days, owned initially by itself. + val faceValue = 10000.DOLLARS `issued by` DUMMY_CASH_ISSUER + val issuance = bigCorpServices.myInfo.legalIdentity.ref(1) + val issueTX: SignedTransaction = + CommercialPaper().generateIssue(issuance, faceValue, TEST_TX_TIME + 30.days, DUMMY_NOTARY).apply { + setTime(TEST_TX_TIME, 30.seconds) + signWith(bigCorpServices.key) + signWith(DUMMY_NOTARY_KEY) + }.toSignedTransaction() + databaseTransaction(databaseAlice) { // Alice pays $9000 to BigCorp to own some of their debt. - val moveTX: SignedTransaction = run { + moveTX = run { val ptx = TransactionType.General.Builder(DUMMY_NOTARY) aliceVaultService.generateSpend(ptx, 9000.DOLLARS, bigCorpServices.key.public) CommercialPaper().generateMove(ptx, issueTX.tx.outRef(0), aliceServices.key.public) @@ -261,7 +284,9 @@ class CommercialPaperTestsGeneric { ptx.signWith(DUMMY_NOTARY_KEY) ptx.toSignedTransaction() } + } + databaseTransaction(databaseBigCorp) { fun makeRedeemTX(time: Instant): SignedTransaction { val ptx = TransactionType.General.Builder(DUMMY_NOTARY) ptx.setTime(time, 30.seconds) From bc525aabdf3dadc3d8bb67563a3d36e24ffa2659 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Thu, 27 Oct 2016 16:21:05 +0100 Subject: [PATCH 9/9] FungibleAsset reverted back to original filename. --- .../r3corda/core/contracts/FungibleAsset.kt | 66 +++++++++++++++++++ .../com/r3corda/core/contracts/Structures.kt | 62 ----------------- 2 files changed, 66 insertions(+), 62 deletions(-) create mode 100644 core/src/main/kotlin/com/r3corda/core/contracts/FungibleAsset.kt diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/FungibleAsset.kt b/core/src/main/kotlin/com/r3corda/core/contracts/FungibleAsset.kt new file mode 100644 index 0000000000..5f6be7908d --- /dev/null +++ b/core/src/main/kotlin/com/r3corda/core/contracts/FungibleAsset.kt @@ -0,0 +1,66 @@ +package com.r3corda.core.contracts + +import java.security.PublicKey + +class InsufficientBalanceException(val amountMissing: Amount<*>) : Exception() { + override fun toString() = "Insufficient balance, missing $amountMissing" +} + +/** + * Interface for contract states representing assets which are fungible, countable and issued by a + * specific party. States contain assets which are equivalent (such as cash of the same currency), + * so records of their existence can be merged or split as needed where the issuer is the same. For + * instance, dollars issued by the Fed are fungible and countable (in cents), barrels of West Texas + * crude are fungible and countable (oil from two small containers can be poured into one large + * container), shares of the same class in a specific company are fungible and countable, and so on. + * + * See [Cash] for an example contract that implements currency using state objects that implement + * this interface. + * + * @param T a type that represents the asset in question. This should describe the basic type of the asset + * (GBP, USD, oil, shares in company , etc.) and any additional metadata (issuer, grade, class, etc.). + */ +interface FungibleAsset : OwnableState { + /** + * Where the underlying asset backing this ledger entry can be found. The reference + * is only intended for use by the issuer, and is not intended to be meaningful to others. + */ + val deposit: PartyAndReference + val issuanceDef: Issued + val amount: Amount> + /** + * There must be an ExitCommand signed by these keys to destroy the amount. While all states require their + * owner to sign, some (i.e. cash) also require the issuer. + */ + val exitKeys: Collection + /** There must be a MoveCommand signed by this key to claim the amount */ + override val owner: PublicKey + fun move(newAmount: Amount>, newOwner: PublicKey): FungibleAsset + + // Just for grouping + interface Commands : CommandData { + interface Move : MoveCommand, Commands + + /** + * Allows new asset states to be issued into existence: the nonce ("number used once") ensures the transaction + * has a unique ID even when there are no inputs. + */ + interface Issue : IssueCommand, Commands + + /** + * A command stating that money has been withdrawn from the shared ledger and is now accounted for + * in some other way. + */ + interface Exit : Commands { val amount: Amount> } + } +} + + +// Small DSL extensions. + +/** Sums the asset states in the list, returning null if there are none. */ +fun Iterable.sumFungibleOrNull() = filterIsInstance>().map { it.amount }.sumOrNull() + +/** Sums the asset states in the list, returning zero of the given token if there are none. */ +fun Iterable.sumFungibleOrZero(token: Issued) = filterIsInstance>().map { it.amount }.sumOrZero(token) + diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt b/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt index 015d738f36..61ad26b0eb 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt @@ -468,66 +468,4 @@ interface Attachment : NamedByHash { } } -class InsufficientBalanceException(val amountMissing: Amount<*>) : Exception() { - override fun toString() = "Insufficient balance, missing $amountMissing" -} - -/** - * Interface for contract states representing assets which are fungible, countable and issued by a - * specific party. States contain assets which are equivalent (such as cash of the same currency), - * so records of their existence can be merged or split as needed where the issuer is the same. For - * instance, dollars issued by the Fed are fungible and countable (in cents), barrels of West Texas - * crude are fungible and countable (oil from two small containers can be poured into one large - * container), shares of the same class in a specific company are fungible and countable, and so on. - * - * See [Cash] for an example contract that implements currency using state objects that implement - * this interface. - * - * @param T a type that represents the asset in question. This should describe the basic type of the asset - * (GBP, USD, oil, shares in company , etc.) and any additional metadata (issuer, grade, class, etc.). - */ -interface FungibleAsset : OwnableState { - /** - * Where the underlying asset backing this ledger entry can be found. The reference - * is only intended for use by the issuer, and is not intended to be meaningful to others. - */ - val deposit: PartyAndReference - val issuanceDef: Issued - val amount: Amount> - /** - * There must be an ExitCommand signed by these keys to destroy the amount. While all states require their - * owner to sign, some (i.e. cash) also require the issuer. - */ - val exitKeys: Collection - /** There must be a MoveCommand signed by this key to claim the amount */ - override val owner: PublicKey - fun move(newAmount: Amount>, newOwner: PublicKey): FungibleAsset - - // Just for grouping - interface Commands : CommandData { - interface Move : MoveCommand, Commands - - /** - * Allows new asset states to be issued into existence: the nonce ("number used once") ensures the transaction - * has a unique ID even when there are no inputs. - */ - interface Issue : IssueCommand, Commands - - /** - * A command stating that money has been withdrawn from the shared ledger and is now accounted for - * in some other way. - */ - interface Exit : Commands { val amount: Amount> } - } -} - - -// Small DSL extensions. - -/** Sums the asset states in the list, returning null if there are none. */ -fun Iterable.sumFungibleOrNull() = filterIsInstance>().map { it.amount }.sumOrNull() - -/** Sums the asset states in the list, returning zero of the given token if there are none. */ -fun Iterable.sumFungibleOrZero(token: Issued) = filterIsInstance>().map { it.amount }.sumOrZero(token) -