FungibleState and design document for tokens (#4049)

This commit is contained in:
Roger Willis 2018-10-20 10:52:24 +01:00 committed by GitHub
parent 3a8fd51a08
commit dd60ae27f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 186 additions and 25 deletions

View File

@ -561,7 +561,7 @@ public final class net.corda.core.contracts.ContractsDSL extends java.lang.Objec
public static final java.util.List<net.corda.core.contracts.CommandWithParties<C>> select(java.util.Collection<? extends net.corda.core.contracts.CommandWithParties<? extends net.corda.core.contracts.CommandData>>, Class<C>, java.util.Collection<? extends java.security.PublicKey>, java.util.Collection<net.corda.core.identity.Party>)
##
@CordaSerializable
public interface net.corda.core.contracts.FungibleAsset extends net.corda.core.contracts.OwnableState
public interface net.corda.core.contracts.FungibleAsset extends net.corda.core.contracts.FungibleState, net.corda.core.contracts.OwnableState
@NotNull
public abstract net.corda.core.contracts.Amount<net.corda.core.contracts.Issued<T>> getAmount()
@NotNull
@ -569,6 +569,11 @@ public interface net.corda.core.contracts.FungibleAsset extends net.corda.core.c
@NotNull
public abstract net.corda.core.contracts.FungibleAsset<T> withNewOwnerAndAmount(net.corda.core.contracts.Amount<net.corda.core.contracts.Issued<T>>, net.corda.core.identity.AbstractParty)
##
@CordaSerializable
public interface net.corda.core.contracts.FungibleState extends net.corda.core.contracts.ContractState
@NotNull
public abstract net.corda.core.contracts.Amount<T> getAmount()
##
@DoNotImplement
@CordaSerializable
public final class net.corda.core.contracts.HashAttachmentConstraint extends java.lang.Object implements net.corda.core.contracts.AttachmentConstraint
@ -3366,7 +3371,7 @@ public interface net.corda.core.node.services.VaultService
public abstract net.corda.core.messaging.DataFeed<net.corda.core.node.services.Vault$Page<T>, net.corda.core.node.services.Vault$Update<T>> trackBy(Class<? extends T>, net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.Sort)
@Suspendable
@NotNull
public abstract java.util.List<net.corda.core.contracts.StateAndRef<T>> tryLockFungibleStatesForSpending(java.util.UUID, net.corda.core.node.services.vault.QueryCriteria, net.corda.core.contracts.Amount<U>, Class<? extends T>)
public abstract java.util.List<net.corda.core.contracts.StateAndRef<T>> tryLockFungibleStatesForSpending(java.util.UUID, net.corda.core.node.services.vault.QueryCriteria, net.corda.core.contracts.Amount<?>, Class<? extends T>)
@NotNull
public abstract net.corda.core.concurrent.CordaFuture<net.corda.core.node.services.Vault$Update<net.corda.core.contracts.ContractState>> whenConsumed(net.corda.core.contracts.StateRef)
##

View File

@ -28,12 +28,12 @@ class InsufficientBalanceException(val amountMissing: Amount<*>) : FlowException
* (GBP, USD, oil, shares in company <X>, etc.) and any additional metadata (issuer, grade, class, etc.).
*/
@KeepForDJVM
interface FungibleAsset<T : Any> : OwnableState {
interface FungibleAsset<T : Any> : FungibleState<Issued<T>>, OwnableState {
/**
* Amount represents a positive quantity of some issued product which can be cash, tokens, assets, or generally
* anything else that's quantifiable with integer quantities. See [Issued] and [Amount] for more details.
*/
val amount: Amount<Issued<T>>
override val amount: Amount<Issued<T>>
/**
* There must be an ExitCommand signed by these keys to destroy the amount. While all states require their

View File

@ -0,0 +1,37 @@
package net.corda.core.contracts
import net.corda.core.KeepForDJVM
/**
* Interface to represent things which are fungible, this means that there is an expectation that these things can
* be split and merged. That's the only assumption made by this interface.
*
* This interface has been defined in addition to [FungibleAsset] to provide some additional flexibility which
* [FungibleAsset] lacks, in particular:
*
* - [FungibleAsset] defines an amount property of type Amount<Issued<T>>, therefore there is an assumption that all
* fungible things are issued by a single well known party but this is not always the case. For example,
* crypto-currencies like Bitcoin are generated periodically by a pool of pseudo-anonymous miners
* and Corda can support such crypto-currencies.
* - [FungibleAsset] implements [OwnableState], as such there is an assumption that all fungible things are ownable.
* This is not always true as fungible derivative contracts exist, for example.
*
* The expectation is that this interface should be combined with the other core state interfaces such as
* [OwnableState] and others created at the application layer.
*
* @param T a type that represents the fungible thing 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.). An
* upper-bound is not specified for [T] to ensure flexibility. Typically, a class would be provided that implements
* [TokenizableAssetInfo].
*/
// DOCSTART 1
@KeepForDJVM
interface FungibleState<T : Any> : ContractState {
/**
* Amount represents a positive quantity of some token which can be cash, tokens, stock, agreements, or generally
* anything else that's quantifiable with integer quantities. See [Amount] for more details.
*/
val amount: Amount<T>
}
// DOCEND 1

View File

@ -334,24 +334,26 @@ interface VaultService {
/**
* Helper function to determine spendable states and soft locking them.
* Currently performance will be worse than for the hand optimised version in `Cash.unconsumedCashStatesForSpending`.
* However, this is fully generic and can operate with custom [FungibleAsset] states.
* Currently performance will be worse than for the hand optimised version in
* [Cash.unconsumedCashStatesForSpending]. However, this is fully generic and can operate with custom [FungibleState]
* and [FungibleAsset] states.
* @param lockId The [FlowLogic.runId]'s [UUID] of the current flow used to soft lock the states.
* @param eligibleStatesQuery A custom query object that selects down to the appropriate subset of all states of the
* [contractStateType]. e.g. by selecting on account, issuer, etc. The query is internally augmented with the
* [StateStatus.UNCONSUMED], soft lock and contract type requirements.
* @param amount The required amount of the asset, but with the issuer stripped off.
* It is assumed that compatible issuer states will be filtered out by the [eligibleStatesQuery].
* @param amount The required amount of the asset. It is assumed that compatible issuer states will be filtered out
* by the [eligibleStatesQuery]. This method accepts both Amount<Issued<*>> and Amount<*>. Amount<Issued<*>> is
* automatically unwrapped to Amount<*>.
* @param contractStateType class type of the result set.
* @return Returns a locked subset of the [eligibleStatesQuery] sufficient to satisfy the requested amount,
* or else an empty list and no change in the stored lock states when their are insufficient resources available.
*/
@Suspendable
@Throws(StatesNotAvailableException::class)
fun <T : FungibleAsset<U>, U : Any> tryLockFungibleStatesForSpending(lockId: UUID,
eligibleStatesQuery: QueryCriteria,
amount: Amount<U>,
contractStateType: Class<out T>): List<StateAndRef<T>>
fun <T : FungibleState<*>> tryLockFungibleStatesForSpending(lockId: UUID,
eligibleStatesQuery: QueryCriteria,
amount: Amount<*>,
contractStateType: Class<out T>): List<StateAndRef<T>>
// DOCSTART VaultQueryAPI
/**

View File

@ -168,6 +168,22 @@ sealed class QueryCriteria : GenericQueryCriteria<QueryCriteria, IQueryCriteriaP
}
}
/**
* FungibleStateQueryCriteria: provides query by attributes defined in [VaultSchema.VaultFungibleStates]
*/
data class FungibleStateQueryCriteria(
val participants: List<AbstractParty>? = null,
val quantity: ColumnPredicate<Long>? = null,
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
override val contractStateTypes: Set<Class<out ContractState>>? = null,
override val relevancyStatus: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL
) : CommonQueryCriteria() {
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
super.visit(parser)
return parser.parseCriteria(this)
}
}
/**
* FungibleStateQueryCriteria: provides query by attributes defined in [VaultSchema.VaultFungibleStates]
*/

View File

@ -100,6 +100,37 @@ Because ``OwnableState`` models fungible assets that can be merged and split ove
not have a ``linearId``. $5 of cash created by one transaction is considered to be identical to $5 of cash produced by
another transaction.
FungibleState
~~~~~~~~~~~~~
`FungibleState<T>` is an interface to represent things which are fungible, this means that there is an expectation that
these things can be split and merged. That's the only assumption made by this interface. This interface should be
implemented if you want to represent fractional ownership in a thing, or if you have many things. Examples:
* There is only one Mona Lisa which you wish to issue 100 tokens, each representing a 1% interest in the Mona Lisa
* A company issues 1000 shares with a nominal value of 1, in one batch of 1000. This means the single batch of 1000
shares could be split up into 1000 units of 1 share.
The interface is defined as follows:
.. container:: codeset
.. literalinclude:: ../../core/src/main/kotlin/net/corda/core/contracts/FungibleState.kt
:language: kotlin
:start-after: DOCSTART 1
:end-before: DOCEND 1
As seen, the interface takes a type parameter `T` that represents the fungible thing in question. This should describe
the basic type of the asset e.g. GBP, USD, oil, shares in company <X>, etc. and any additional metadata (issuer, grade,
class, etc.). An upper-bound is not specified for `T` to ensure flexibility. Typically, a class would be provided that
implements `TokenizableAssetInfo` so the thing can be easily added and subtracted using the `Amount` class.
This interface has been added in addition to `FungibleAsset` to provide some additional flexibility which
`FungibleAsset` lacks, in particular:
* `FungibleAsset` defines an amount property of type Amount<Issued<T>>, therefore there is an assumption that all
fungible things are issued by a single well known party but this is not always the case.
* `FungibleAsset` implements `OwnableState`, as such there is an assumption that all fungible things are ownable.
Other interfaces
^^^^^^^^^^^^^^^^
You can also customize your state by implementing the following interfaces:

View File

@ -225,6 +225,15 @@ Unreleased
normal state when it occurs in an input or output position. *This feature is only available on Corda networks running
with a minimum platform version of 4.*
* Removed type parameter `U` from `tryLockFungibleStatesForSpending` to allow the function to be used with `FungibleState`
as well as `FungibleAsset`. This _might_ cause a compile failure in some obscure cases due to the removal of the type
parameter from the method. If your CorDapp does specify types explicitly when using this method then updating the types
will allow your app to compile successfully. However, those using type inference (e.g. using Kotlin) should not experience
any changes. Old CorDapp JARs will still work regardless.
* `issuer_ref` column in `FungibleStateSchema` was updated to be nullable to support the introduction of the
`FungibleState` interface. The `vault_fungible_states` table can hold both `FungibleAssets` and `FungibleStates`.
Version 3.3
-----------

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

After

Width:  |  Height:  |  Size: 171 KiB

View File

@ -2,6 +2,7 @@ package net.corda.node.services.schema
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.FungibleAsset
import net.corda.core.contracts.FungibleState
import net.corda.core.contracts.LinearState
import net.corda.core.schemas.*
import net.corda.core.schemas.MappedSchemaValidator.crossReferencesToOtherMappedSchema
@ -65,6 +66,8 @@ class NodeSchemaService(private val extraSchemas: Set<MappedSchema> = emptySet()
if (state is LinearState)
schemas += VaultSchemaV1 // VaultLinearStates
if (state is FungibleAsset<*>)
schemas += VaultSchemaV1 // VaultFungibleAssets
if (state is FungibleState<*>)
schemas += VaultSchemaV1 // VaultFungibleStates
return schemas
@ -76,6 +79,14 @@ class NodeSchemaService(private val extraSchemas: Set<MappedSchema> = emptySet()
return VaultSchemaV1.VaultLinearStates(state.linearId, state.participants)
if ((schema === VaultSchemaV1) && (state is FungibleAsset<*>))
return VaultSchemaV1.VaultFungibleStates(state.owner, state.amount.quantity, state.amount.token.issuer.party, state.amount.token.issuer.reference, state.participants)
if ((schema === VaultSchemaV1) && (state is FungibleState<*>))
return VaultSchemaV1.VaultFungibleStates(
participants = state.participants.toMutableSet(),
owner = null,
quantity = state.amount.quantity,
issuer = null,
issuerRef = null
)
return (state as QueryableState).generateMappedObject(schema)
}

View File

@ -10,13 +10,10 @@ import net.corda.core.messaging.DataFeed
import net.corda.core.node.ServicesForResolution
import net.corda.core.node.StatesToRecord
import net.corda.core.node.services.*
import net.corda.core.node.services.vault.*
import net.corda.core.node.services.Vault.ConstraintInfo.Companion.constraintInfo
import net.corda.core.node.services.vault.*
import net.corda.core.schemas.PersistentStateRef
import net.corda.core.serialization.SerializationDefaults.STORAGE_CONTEXT
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.core.transactions.*
import net.corda.core.utilities.*
import net.corda.node.services.api.SchemaService
@ -278,7 +275,10 @@ class NodeVaultService(
val uuid = (Strand.currentStrand() as? FlowStateMachineImpl<*>)?.id?.uuid
val vaultUpdate = if (uuid != null) netUpdate.copy(flowId = uuid) else netUpdate
if (uuid != null) {
val fungible = netUpdate.produced.filter { it.state.data is FungibleAsset<*> }
val fungible = netUpdate.produced.filter { stateAndRef ->
val state = stateAndRef.state.data
state is FungibleAsset<*> || state is FungibleState<*>
}
if (fungible.isNotEmpty()) {
val stateRefs = fungible.map { it.ref }.toNonEmptySet()
log.trace { "Reserving soft locks for flow id $uuid and states $stateRefs" }
@ -397,14 +397,27 @@ class NodeVaultService(
@Suspendable
@Throws(StatesNotAvailableException::class)
override fun <T : FungibleAsset<U>, U : Any> tryLockFungibleStatesForSpending(lockId: UUID,
eligibleStatesQuery: QueryCriteria,
amount: Amount<U>,
contractStateType: Class<out T>): List<StateAndRef<T>> {
override fun <T : FungibleState<*>> tryLockFungibleStatesForSpending(
lockId: UUID,
eligibleStatesQuery: QueryCriteria,
amount: Amount<*>,
contractStateType: Class<out T>
): List<StateAndRef<T>> {
if (amount.quantity == 0L) {
return emptyList()
}
// Helper to unwrap the token from the Issued object if one exists.
fun unwrapIssuedAmount(amount: Amount<*>): Any {
val token = amount.token
return when (token) {
is Issued<*> -> token.product
else -> token
}
}
val unwrappedToken = unwrapIssuedAmount(amount)
// Enrich QueryCriteria with additional default attributes (such as soft locks).
// We only want to return RELEVANT states here.
val sortAttribute = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF)
@ -419,8 +432,10 @@ class NodeVaultService(
var claimedAmount = 0L
val claimedStates = mutableListOf<StateAndRef<T>>()
for (state in results.states) {
val issuedAssetToken = state.state.data.amount.token
if (issuedAssetToken.product == amount.token) {
// This method handles Amount<Issued<T>> in FungibleAsset and Amount<T> in FungibleState.
val issuedAssetToken = unwrapIssuedAmount(state.state.data.amount)
if (issuedAssetToken == unwrappedToken) {
claimedStates += state
claimedAmount += state.state.data.amount.quantity
if (claimedAmount > amount.quantity) {

View File

@ -145,9 +145,9 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio
@Column(name = "issuer_name", nullable = true)
var issuer: AbstractParty?,
@Column(name = "issuer_ref", length = MAX_ISSUER_REF_SIZE, nullable = false)
@Column(name = "issuer_ref", length = MAX_ISSUER_REF_SIZE, nullable = true)
@Type(type = "corda-wrapper-binary")
var issuerRef: ByteArray
var issuerRef: ByteArray?
) : PersistentState() {
constructor(_owner: AbstractParty, _quantity: Long, _issuerParty: AbstractParty, _issuerRef: OpaqueBytes, _participants: List<AbstractParty>) :
this(owner = _owner,

View File

@ -10,4 +10,5 @@
<include file="migration/vault-schema.changelog-pkey.xml"/>
<include file="migration/vault-schema.changelog-v5.xml"/>
<include file="migration/vault-schema.changelog-v6.xml"/>
<include file="migration/vault-schema.changelog-v7.xml"/>
</databaseChangeLog>

View File

@ -0,0 +1,8 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
<changeSet author="R3.Corda" id="make_issuer_ref_nullable">
<dropNotNullConstraint columnDataType="varbinary(512)" columnName="issuer_ref" tableName="vault_fungible_states"/>
</changeSet>
</databaseChangeLog>

View File

@ -129,6 +129,32 @@ class NodeVaultServiceTest {
return tryLockFungibleStatesForSpending(lockId, baseCriteria, amount, Cash.State::class.java)
}
@Test
fun `fungible state selection test`() {
val issuerParty = services.myInfo.legalIdentities.first()
class FungibleFoo(override val amount: Amount<Currency>, override val participants: List<AbstractParty>) : FungibleState<Currency>
val fungibleFoo = FungibleFoo(100.DOLLARS, listOf(issuerParty))
services.apply {
val tx = signInitialTransaction(TransactionBuilder(DUMMY_NOTARY).apply {
addCommand(Command(DummyContract.Commands.Create(), issuerParty.owningKey))
addOutputState(fungibleFoo, DummyContract.PROGRAM_ID)
})
recordTransactions(listOf(tx))
}
val baseCriteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(notary = listOf(DUMMY_NOTARY))
database.transaction {
val states = services.vaultService.tryLockFungibleStatesForSpending(
lockId = UUID.randomUUID(),
eligibleStatesQuery = baseCriteria,
amount = 10.DOLLARS,
contractStateType = FungibleFoo::class.java
)
assertEquals(states.single().state.data.amount, 100.DOLLARS)
}
}
@Test
fun `duplicate insert of transaction does not fail`() {
database.transaction {