mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
FungibleState and design document for tokens (#4049)
This commit is contained in:
parent
3a8fd51a08
commit
dd60ae27f2
@ -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)
|
||||
##
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
/**
|
||||
|
@ -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]
|
||||
*/
|
||||
|
@ -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:
|
||||
|
@ -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 |
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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>
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user