mirror of
https://github.com/corda/corda.git
synced 2024-12-19 04:57:58 +00:00
Re-factoring of CashBalances code (moved to VaultService)
This commit is contained in:
parent
d7ca215f7d
commit
67b2d91b33
@ -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.*
|
||||
|
@ -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
|
||||
|
@ -196,18 +196,6 @@ fun Iterable<ContractState>.sumCashOrZero(currency: Issued<Currency>): Amount<Is
|
||||
return filterIsInstance<Cash.State>().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<Currency, Amount<Currency>> 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<Currency, List<Amount>> like { GBP -> (£100, £500, etc), USD -> ($2000, $50) }
|
||||
groupBy { it.token.product }.
|
||||
// Collapse to Map<Currency, Amount> 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)))
|
||||
|
@ -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 <X>, etc.) and any additional metadata (issuer, grade, class, etc.).
|
||||
*/
|
||||
interface FungibleAsset<T> : 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<T>
|
||||
val amount: Amount<Issued<T>>
|
||||
/**
|
||||
* 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<PublicKey>
|
||||
/** There must be a MoveCommand signed by this key to claim the amount */
|
||||
override val owner: PublicKey
|
||||
fun move(newAmount: Amount<Issued<T>>, newOwner: PublicKey): FungibleAsset<T>
|
||||
|
||||
// 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<T> : Commands { val amount: Amount<Issued<T>> }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Small DSL extensions.
|
||||
|
||||
/** Sums the asset states in the list, returning null if there are none. */
|
||||
fun <T> Iterable<ContractState>.sumFungibleOrNull() = filterIsInstance<FungibleAsset<T>>().map { it.amount }.sumOrNull()
|
||||
|
||||
/** Sums the asset states in the list, returning zero of the given token if there are none. */
|
||||
fun <T> Iterable<ContractState>.sumFungibleOrZero(token: Issued<T>) = filterIsInstance<FungibleAsset<T>>().map { it.amount }.sumOrZero(token)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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() }
|
||||
|
@ -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() }
|
||||
|
@ -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 <X>, etc.) and any additional metadata (issuer, grade, class, etc.).
|
||||
*/
|
||||
interface FungibleAsset<T> : 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<T>
|
||||
val amount: Amount<Issued<T>>
|
||||
/**
|
||||
* 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<PublicKey>
|
||||
/** There must be a MoveCommand signed by this key to claim the amount */
|
||||
override val owner: PublicKey
|
||||
fun move(newAmount: Amount<Issued<T>>, newOwner: PublicKey): FungibleAsset<T>
|
||||
|
||||
// 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<T> : Commands { val amount: Amount<Issued<T>> }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Small DSL extensions.
|
||||
|
||||
/** Sums the asset states in the list, returning null if there are none. */
|
||||
fun <T> Iterable<ContractState>.sumFungibleOrNull() = filterIsInstance<FungibleAsset<T>>().map { it.amount }.sumOrNull()
|
||||
|
||||
/** Sums the asset states in the list, returning zero of the given token if there are none. */
|
||||
fun <T> Iterable<ContractState>.sumFungibleOrZero(token: Issued<T>) = filterIsInstance<FungibleAsset<T>>().map { it.amount }.sumOrZero(token)
|
||||
|
||||
|
||||
|
@ -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<Vault.Update>
|
||||
|
||||
/**
|
||||
* 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<Currency, Amount<Currency>>
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
@ -40,6 +40,16 @@ open class InMemoryVaultService(protected val services: ServiceHub) : SingletonS
|
||||
override val updates: Observable<Vault.Update>
|
||||
get() = mutex.content._updatesPublisher
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override val cashBalances: Map<Currency, Amount<Currency>>
|
||||
get() = currentVault.states.
|
||||
// Select the states we own which are cash, ignore the rest, take the amounts.
|
||||
mapNotNull { (it.state.data as? FungibleAsset<Currency>)?.amount }.
|
||||
// Turn into a Map<Currency, List<Amount>> like { GBP -> (£100, £500, etc), USD -> ($2000, $50) }
|
||||
groupBy { it.token.product }.
|
||||
// Collapse to Map<Currency, Amount> 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<Vault, Observable<Vault.Update>> {
|
||||
return mutex.locked {
|
||||
Pair(vault, updates.bufferUntilSubscribed())
|
||||
@ -130,4 +140,5 @@ open class InMemoryVaultService(protected val services: ServiceHub) : SingletonS
|
||||
|
||||
return Pair(Vault(newStates), change)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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<Currency, BalanceMetric>()
|
||||
|
||||
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.
|
||||
//
|
||||
|
@ -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<Vault.Update>
|
||||
get() = mutex.locked { _updatesPublisher }
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override val cashBalances: Map<Currency, Amount<Currency>>
|
||||
get() = currentVault.states.
|
||||
// Select the states we own which are cash, ignore the rest, take the amounts.
|
||||
mapNotNull { (it.state.data as? FungibleAsset<Currency>)?.amount }.
|
||||
// Turn into a Map<Currency, List<Amount>> like { GBP -> (£100, £500, etc), USD -> ($2000, $50) }
|
||||
groupBy { it.token.product }.
|
||||
// Collapse to Map<Currency, Amount> 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<Vault, Observable<Vault.Update>> {
|
||||
return mutex.locked {
|
||||
Pair(Vault(allUnconsumedStates()), _updatesPublisher.bufferUntilSubscribed())
|
||||
|
@ -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.
|
||||
}
|
||||
|
@ -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()}")
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user