Skip to content

Latest commit

 

History

History
472 lines (347 loc) · 18.3 KB

IWantTo.md

File metadata and controls

472 lines (347 loc) · 18.3 KB

How do I?

Create fungible and non-fungible tokens

Creating an instance of the TokenType

Two TokenType helpers already exist in the token SDK, FiatCurrency and DigitalCurrency. There are easy to use utilities for both, for example:

    val pounds: TokenType = GBP
    val euros: TokenType = EUR
    val bitcoin: TokenType = BTC

Creating your own tokens is easy; just create an instance of TokenType class. You will need to specify a tokenIdentifier property and how many fractionDigits amounts of this token can have. E.g.

  • "0" for zero fraction digits where there can only exist whole numbers of your token type, and
  • "2" for two decimal places like GBP, USD and EUR

You can also add a toString override, if you like.

    val myTokenType: TokenType = TokenType(tokenIdentifier = "TEST", fractionDigits = 2)

The tokenIdentifier is used along with the tokenClass property (defined in TokenType) when serializing token types. Two properties are required, as with FiatCurrency and DigitalCurrency, there can be many different instances of one tokenClass, each with their own tokenIdentifier.

Creating an instance of an IssuedTokenType

Create an IssuedTokenType as follows

    // With your own instance of token type.
    val issuer: Party = ...
    val myTokenType = TokenType("MyToken", 2)
    val issuedTokenType: IssuedTokenType = myTokenType issuedBy issuer

    // Or with the built in tokens.
    val issuedGbp: IssuedTokenType = GBP issuedBy issuer
    val issuedGbp: IssuedTokenType  = BTC issuedBy issuer

The issuing party must be a Party as opposed to an AbstractParty or AnonymousParty, this is because the issuer must be well known.

The issuedBy syntax uses a kotlin infix extension function.

Creating an amount of some IssuedTokenType

Once you have an IssuedTokenType you can optionally create some amount of it using the of syntax. For example:

    val issuer: Party = ...
    val myTokenType = TokenType("MyToken", 2)
    val myIssuedTokenType: IssuedTokenType = myTokenType issuedBy issuer
    val tenOfMyIssuedTokenType = 10 of myIssuedTokenType

Or:

    val tenPounds: Amount<IssuedTokenType> = 10 of GBP issuedBy issuer

Or:

    val tenPounds = 10.GBP issuedBy issuer

If you do not need to create amounts of your token because it is always intended to be issued as a NonFungibleToken then you don't have to create amounts of it.

Creating instances of FungibleTokens and NonFungibleTokens

To get from a token type or some amount of a token type to a non-fungible token or fungible token, we need to specify which party the proposed holder is. This can be done using the heldBy syntax:

    val issuer: Party = ...
    val holder: Party = ...

    val myTokenType = TokenType("MyToken", 2)
    val myIssuedTokenType: IssuedTokenType = myTokenType issuedBy issuer
    val tenOfMyIssuedTokenType = 10 of myIssuedTokenType

    // Adding a holder to an amount of a token type, creates a fungible token.
    val fungibleToken: FungibleToken = tenOfMyIssuedTokenType heldBy holder
    // Adding a holder to a token type, creates a non-fungible token.
    val nonFungibleToken: NonFungibleToken = myIssuedTokenType heldBy holder

Once you have a FungibleToken or a NonFungibleToken, you can then go and issue that token on ledger.

Use flows from token-sdk? Issue, move and redeem tokens.

All the common flows that come with token-sdk have different versions. We tried to predict most common use cases for issue, move and redeem. So all of the flows below come in fungible and non-fungible, confidential, inlined and initiating versions (only redeem has slightly different form, it doesn't have confidential non-fungible flow as there are never any change outputs to handle).

Let's take a look how these behaviours are different. While fungible and non-fungible versions are straightforward we need some more explanation about inline and initiating flows. When do we use them? General rule is that when a flow is a part of more complicated business logic that involves opening many sessions before it's called, then we could reuse already opened sessions, you should use inline versions of that flow. If you use wrong initiating version of the flow, you will get an error message indicating that there was mismatch in expected send/receive order. Initiating flows start sessions with counterparties after passing the Party objects as parameters.

Additionally, all flows take observer sessions (or observer parties in initiating versions). Observers become important in transaction finalisation step. Transaction will be broadcasted to the participants as well as to the observers to be recorded in the vault (recording strategy for the observers is StatesToRecord.ALL_VISIBLE).

Confidential versions of flows generate and swap new keys for the parties involved in a transaction. These new identities will be used in output states.

If more control over what is in the transaction is needed token-sdk provides useful utilities that take TransactionBuilder and addMoveTokens, addIssueTokens, addRedeemTokens can be called depending what kind of transaction developer wants to construct. Developer is responsible for generating confidential identities, finalisation and possible updating of distribution lists (see section below on that).

It is possible to specify a preferred notary from the list in network parameters. On default it is taken from cordapp configuration file (include notary string with X500 name of the notary you wish to use). If no preferred notary is specified then the first one will be used.

Issuing tokens

Usage

For issue tokens we don't differentiate between fungible and non-fungible flows. Issue flows take AbstractToken as parameter. What it does internally is pretty simple, all the flows below construct an issuance transaction (no inputs, only outputs and IssueTokenCommand) and finalise it with participants and possible observers. Distribution lists get updated after finalisation. Call these flows for one instance of TokenType at a time. If you need to do multiple instances of token type in one transaction then create a new flow, calling addIssueTokens for each token. Confidential versions additionally request that the recipients generate new keys for the output holders.

Initiating

// As in previous examples
val fungibleToken: FungibleToken = ...
val nonFungibleToken: NonFungibleToken = ...
// Start flows via RPC or as a subFlow (it starts a new session with a holder of the token!)
// All of the below flows can take a list of observer parties.
// Fungible
IssueTokens(fungibleToken)
IssueTokens(10 of myTokenType, issuer, holder)
IssueTokens(10 of myIssuedTokenType, holder)
// Nonfungible
IssueTokens(nonFungibleToken)
IssueTokens(myTokenType, issuer, holder)
IssueTokens(myIssuedTokenType, holder)

There are many other constructor overloads for initiating IssueTokens it's worth investigating the class itself.

Responder flow: IssueTokensHandler

Conidential version: ConfidentialIssueTokens, responder: ConfidentialIssueTokensHandler

Inline

// We need to pass in counterparties sessions.
val holderSession = initateFlow(holder)
...
// All of the below flows can take a list of observer sessions.
// Fungible
subFlow(IssueTokensFlow(fungibleToken, listOf(holderSession)))
// NonFungible
subFlow(IssueTokensFlow(nonFungibleToken, listOf(holderSession)))

There are other constructor overloads worth investigating.

Responder flow: IssueTokensFlowHandler

Confidential inline

// We need to pass in counterparties sessions.
val holderSession = initateFlow(holder)
...
// All of the below flows can take a list of observer sessions.
// Fungible
subFlow(ConfidentialIssueTokensFlow(fungibleToken, listOf(holderSession)))
// NonFungible
subFlow(ConfidentialIssueTokensFlow(nonFungibleToken, listOf(holderSession)))

Responder flow: ConfidentialIssueTokensFlowHandler

Moving tokens

Usage

Move tokens flows fall into one of the two categories: fungible and non-fungible. Both versions differ significantly in how states are selected for spend. For more details see TokenSelection. The common tasks performed are: choosing states to spend from Vault, generating possible change outputs, adding inputs and outputs with new holders to the transaction with MoveTokenCommand, finalisation and distribution list update. Notary is chosen based on the inputs notary. We don't support notary change for now. Call these flows for one TokenType at a time. If you need to do multiple instances of token type in one transaction then create a new flow, calling addMoveTokens for each token. As previously confidential versions generate new identities for use in output states.

Moving tokens fungible tokens

This family of flows chooses held amount of given token from vault. If you want to provide other criteria (for example tokens that come only from one issuer) use queryCriteria. QueryUtilities module provides many useful helpers i.e. tokenAmountWithIssuerCriteria. You can move many tokens to different parties in one transaction, to do so specify map of partiesAndAmounts respectively. As usual you can provide additional observers parties/sessions for finalization with other interested parties on the network.

Initiating

val holder: Party = ...
val otherHolder: Party = ...
val issuer: Party = ...
// Move amount of token to the new holder
MoveFungibleTokens(100 of myTokenType, holder)
// Move different amounts of token to multiple holders
MoveFungibleTokens(listOf(PartyAndAmount(holder, 13 of myTokenType), PartyAndAmount(otherHolder, 44 of myTokenType)))
// Move amount of token issued by particular issuer to the new holder - this is an example of using optional queryCriteria
// parameter.
MoveFungibleTokens(
    partyAndAmount = PartyAndAmount(holder, 5 of myTokenType),
    observers = emptyList<Party>(),
    queryCriteria = tokenAmountWithIssuerCriteria(myTokenType, issuer)
)

Responder flow: MoveFungibleTokensHandler

Confidential version: ConfidentialMoveFungibleTokens, responder: ConfidentialMoveFungibleTokensHandler

Inline

// We need to pass in counterparties sessions.
val holderSession = initateFlow(holder)
val otherHolderSession = initateFlow(otherHolder)
...
// All of the below flows can take a list of observer sessions.
// Construct many moves in one transaction.
subFlow(MoveFungibleTokensFlow(listOf(PartyAndAmount(holder, 13 of myTokenType), PartyAndAmount(otherHolder, 44 of myTokenType)),
    listOf(holderSession, otherHolderSession))
)
// Move only tokens issued by particular issuer.
subFlow(MoveFungibleTokensFlow(
    partyAndAmount = PartyAndAmount(holder, 5 of myTokenType),
    queryCriteria = tokenAmountWithIssuerCriteria(myTokenType, issuer),
    participantSessions = listOf(holderSession, otherHolderSession),
    observers = emptyList<FlowSession>()    
))

Responder flow: MoveTokensFlowHandler

Confidential version: ConfidentialMoveFungibleTokensFlow, responder: ConfidentialMoveTokensFlowHandler

Moving non-fungible tokens

Initiating

val holder: Party = ...
...
// Move non fungible token to the new holder
MoveNonFungibleTokens(PartyAndToken(holder, myTokenType))

Similar to previous examples you can provide queryCriteria and list of observer parties.

Responder flow: MoveNonFungibleTokensHandler

Confidential version: ConfidentialMoveNonFungibleTokens, responder: ConfidentialMoveNonFungibleTokensHandler

Inline

val holderSession = initateFlow(holder)
val observerSession = initatieFlow(observer)
subFlow(MoveNonFungibleTokensFlow(PartyAndToken(holder, myTokenType), listOf(holderSession), listOf(observerSession)))

Responder flow: MoveTokensFlowHandler

Confidential version: ConfidentialMoveNonFungibleTokensFlow, responder: ConfidentialMoveTokensFlowHandler

Redeeming tokens

Usage

Similar to MoveTokens family of flows, redeem flows are either fungible or non-fungible. Difference is in the selection of states to redeem from Vault. For more details see TokenSelection. Transaction constructed has states to redeem as inputs, possible single change output and RedeemTokenCommand with signatures both from holder and issuer.

The most important thing about usage of these flows is that they are initiatied by the holder of the tokens. Initiating party sends transaction proposal with states to redeem and possible change output to the issuer. Issuer is always a well known party, it performs basic checks on the transaction. There is additional step that synchronises any confidential identities from the states to redeem with the issuer (bear in mind that issuer usually isn't involved in confidential move of tokens). After that simple collect signatures and finalisation is done.

Call these flows for one TokenType at a time. If you need to do multiple instances of token type in one transaction then create a new flow, calling addTokensToRedeem, addNonFungibleTokensToRedeem oraddFungibleTokensToRedeem for each token (see RedeemFlowUtilities.kt for more documentation). All of the flows take observer parties/sessions as usual.

The main difference to move flows is that only fungible redeem tokens flows have confidential versions. This is due to the fact no change is paid in non-fungible case, so there are no output states to generate confidential identities for.

Redeeming fungible tokens

Selection is performed using the same utilities used in MoveFungibleTokens.

Initiating

val amountToRedeem = 10.GBP
val issuerParty: Party = ...
val observerParty: Party = ...

// It is also possible to provide custom query criteria for token selection.
RedeemFungibleTokens(amount = amountToRedeem, issuer = issuerParty, observers = listOf(observerParty))

Responder flow: RedeemFungibleTokensHandler

Confidential version: ConfidentialRedeemFungibleTokens, responder: ConfidentialRedeemFungibleTokensHandler

Inline

val issuerSession = initateFlow(issuer)
val observerSession = initatieFlow(observer)
val changeHolder: AbstractParty = ... // It can be either confidential identity belonging to tokens holder or well known identity of holder
// It is also possible to provide custom query criteria for token selection.
subFlow(RedeemFungibleTokensFlow(
    amount = 1000.GBP, 
    issuerSession = issuerSession,
    changeHolder = changeHolder,
    observerSessions = listOf(observerSession)
))

Responder flow: RedeemTokensFlowHandler

Confidential version: ConfidentialRedeemFungibleTokensFlow,

responder: ConfidentialRedeemFungibleTokensFlowHandler

Redeeming non-fungible tokens

Initiating

val myTokenType: TokenType = ...
val issuerParty: Party = ...
val observerParty: Party = ...

RedeemNonFungibleTokens(myTokenType, issuerParty, listOf(observerParty))

Responder flow: RedeemNonFungibleTokensHandler

Inline

val myTokenType: TokenType = ...
val issuerSession = initateFlow(issuerParty)
val observerSession = initatieFlow(observerParty)
subFlow(RedeemNonFungibleTokensFlow(myTokenType, listOf(issuerSession), listOf(observerSession)))

Responder flow: RedeemTokensFlowHandler

No confidential versions - there is no change paid back.

Finalising token transactions

If you wish to construct your token transaction using utility functions instead of using ready flows there are some steps that need to be performed to correctly finalise it.

Calling ObserverAwareTokensFlow

Additionally to normal FinalityFlow we introduced ObserverAwareFinalityFlow that takes additional flow sessions for observer nodes. Observer is an identity that is not a participant in a transaction, but should be informed about it. Observers record states in StatesToRecord.ALL_VISIBLE mode. Participants handle the transaction as usual. You can call it at the finalisation step:

val stx: SignedTransaction = ...
val participantSession: FlowSession = initFlow(participantParty)
val observerSession: FlowSession = initFlow(observerParty)
subFlow(ObserverAwareFinalityFlow(stx, listOf(participantSession, observerSession)))

Keeping distribution lists up-to-date

This is temporary solution for distributing updates to evolvable tokens. It will be changed in the future for more robust design using data distribution groups. For now it is important when using pointers to evolvable tokens to call UpdateDistributionListFlow that takes care of adding new parties to the distribution list kept by the token maintainer (usually it's issuer).

Simply call at the end of your flow:

val stx: SignedTransaction = ...
subFlow(UpdateDistributionListFlow(stx))

Creating your own subtypes of TokenType

If type-safety is required or if you need to define custom properties on top of the tokenIdentifier and fractionDigits then it is still possible to create your own TokenType sub-type by sub-classing TokenType.

class MyTokenType(override val tokenIdentifier: String, override val fractionDigits: Int = 0) : TokenType

The above defined token type, allows CorDapp developers to create multiple instances of the token type with different identifiers, for example:

  • MyTokenType("ABC") -> tokenClass: MyTokenType, tokenIdentifier: ABC
  • MyTokenType("XYZ") -> tokenClass: MyTokenType, tokenIdentifier: XYZ

Creating an instance of your new TokenType

Create an instance of your new token type like you would a regular object.

    val myTokenType = MyTokenType("TEST", 2)

This creates a token of

    val tokenClass: MyTokenType
    val tokenIdentifier: TEST

Similar to the above you can create IssuedTokenType using your new token:

val issuer: Party = ...
val issuedTokenType: IssuedTokenType = myTokenType issuedBy issuer

Specyfing the notary from the notary list in network parameters

To always use notary of your choice in CorDapp, it needs specifying in CorDapp config. Add notary X500 name (one from network parameters):

notary = "O=Notary,L=London,C=GB"

All flows from token-sdk will use this notary. If you want to use it from your custom flows, you can call:

val notary = getPreferredNotary(serviceHub)
// And pass it to transaction builder
TransactionBuilder(notary = notary)