Selection in the Tokens SDK
When you move or redeem tokens using the Tokens SDK, you can choose which balance of tokens you want to use, and how much from each reserve, in any given transaction.
This process is called Selection.
You can write flows for moving your tokens that allow selection from either:
- The database which stores token data.
- In-memory data, which is like a cache of a node’s current token data.
In-memory selection is a much faster method of choosing the right token reserves to use in a transaction. However, you may decide you prefer Database selection as it keeps the database as the only active source of truth for your tokens.
Token selection and soft-locking
Soft Locking is implemented in the vault to try and prevent a node constructing transactions that attempt to use the same input(s) simultaneously. Such transactions would result in naturally wasted work when the notary rejects them as double spend attempts.
Soft locks are automatically applied to coin selection (eg. cash spending) to ensure that no two transactions attempt to spend the same fungible states. The outcome of such an eventuality will result in an InsufficientBalanceException
for one of the requesters if there are insufficient number of fungible states available to satisfy both requests.
In addition, if you use Tokens SDK 1.2.2, token selection also checks whether it would have been possible to satisfy the amount if some of these tokens had not been soft locked. If this would have been possible, then an InsufficientNotLockedBalanceException
is thrown.
Token selection with multithreaded SMM
A multithreaded environment is characterised by running tokens with Corda Enterprise where the number of flow workers is configured to be > 1.
You can only use in-memory selection in a multithreaded environment. This is because a cache of available tokens balances are maintained for querying in the JVM. This means the query time to select available tokens is extremely fast, preventing the need for soft-locking tokens in the DB. Tokens are simply selected, added to a transaction and spent.
In DB selection, token states must be queried from the vault and “selected” by soft-locking the record in the database. This doesn’t work in a multi-threaded environment and multiple threads running at the same time may end up selecting the same token state to be spent. This will lead to the node throwing an InsufficientBalanceException
or InsufficientNotLockedBalanceException
as all available token states (and associated records) are reserved for other concurrent transactions. While this won’t jeopardize data, it could impact the performance of your application.
Move tokens using Database Selection
In the Tokens SDK, database (DB) selection is the default method of selection for each transaction.
In move flows of multiple tokens using database selection, you specify the method of selection to modify the TransactionBuilder
, along with the preferred selection source of payment.
In the example below, multiple fungible token moves are added to a token using DB selection:
@Suspendable
@JvmOverloads
fun addMoveFungibleTokens(
transactionBuilder: TransactionBuilder,
serviceHub: ServiceHub,
partiesAndAmounts: List<PartyAndAmount<TokenType>>,
changeHolder: AbstractParty,
queryCriteria: QueryCriteria? = null
): TransactionBuilder {
// Instantiate a DatabaseTokenSelection class which you will use to select tokens
val selector = DatabaseTokenSelection(serviceHub)
// Use the generateMove utility on the DatabaseTokenSelection class to determine the input and output token states
val (inputs, outputs) = selector.generateMove(partiesAndAmounts.toPairs(), changeHolder, TokenQueryBy(queryCriteria = queryCriteria), transactionBuilder.lockId)
// Add those input and output token states to the transaction
// This step also calculates and adds the appropriate commands to the transaction so that Token contract verification rules may be applied
return addMoveTokens(transactionBuilder = transactionBuilder, inputs = inputs, outputs = outputs)
}
Move tokens using in-memory selection
You can use in-memory token in much the same way as DB selection, you are able to call the generateMove
method to select tokens available for the transaction being constructed.
In the example below where the only change is LocalTokenSelector
in place of DBTokenSelector
.
You can see that the addMoveFungibleTokens
defaults to database selection. If you wish to use in-memory selection you should write your own utility method, like this:
@Suspendable
@JvmOverloads
fun addMoveFungibleTokensInMemory(
transactionBuilder: TransactionBuilder,
serviceHub: ServiceHub,
partiesAndAmounts: List<PartyAndAmount<TokenType>>,
changeHolder: AbstractParty,
queryCriteria: QueryCriteria? = null
): TransactionBuilder {
// Instantiate a LocalTokenSelection class which you will use to select tokens
val selector = LocalTokenSelection(serviceHub)
// Use the generateMove utility on the DatabaseTokenSelection class to determine the input and output token states
val (inputs, outputs) = selector.generateMove(partiesAndAmounts.toPairs(), changeHolder, TokenQueryBy(queryCriteria = queryCriteria), transactionBuilder.lockId)
// Add those input and output token states to the transaction
// This step also calculates and adds the appropriate commands to the transaction so that Token contract verification rules may be applied
return addMoveTokens(transactionBuilder = transactionBuilder, inputs = inputs, outputs = outputs)
}
MoveTokensFlow
or addMoveTokens
(not addMoveFungibleTokens
), because you already performed selection and provide input and output states directly. addMoveFungibleTokens
must always use database selection.Initialise VaultWatcherService
To use in-memory selection, you must ensure the CorDapp VaultWatcherService
is installed and the service is running. This comes as part of the Tokens SDK.
To initialise this service, you must specify the indexingStrategies
property. An indexing strategy is used to apply an index to recorded records of Token States in in the VaultWatcherService
. This improves querying time (and ultimately the performance of your application). As always - you can tune different use cases for better performance by selecting the appropriate indexing strategy:
- External_ID strategy can be used to group states from many public keys connected to a given unique user ID. If you use Accounts, this strategy is ideal because it allows for faster querying of tokens that belong to accounts.
- Public_key strategy makes a token ‘bucket’ for each public key.
Enter the following into your CorDapp config, choosing a single indexing strategy:
stateSelection {
inMemory {
indexingStrategies: ["EXTERNAL_ID"|"PUBLIC_KEY"]
cacheSize: Int
}
}
In this example, token selection is configured in the deployNodes
with PUBLIC_KEY
as the indexing strategy, added under task deployNodes(type: net.corda.plugins.Cordform)
.
nodeDefaults {
cordapp ("$corda_tokens_sdk_release_group:tokens-selection:$corda_tokens_sdk_version"){
config '''
stateSelection {
inMemory {
indexingStrategies: ["PUBLIC_KEY"]
cacheSize: 1024
}
}
'''
}
}
Redeem tokens using LocalTokenSelection
You can create a flow for redeeming tokens using LocalTokenSelection
in a similar way to moving tokens:
Choose states that cover the required amount.
Create exit states and get possible change outputs.
Call the subflow to redeem states with the issuer.
val vaultWatcherService = serviceHub.cordaService(VaultWatcherService::class.java)
val localTokenSelector = LocalTokenSelector(serviceHub, vaultWatcherService, autoUnlockDelay = autoUnlockDelay)
// Choose states that cover the required amount.
val exitStates: List<StateAndRef<FungibleToken>> = localTokenSelector.selectStates(
lockID = transactionBuilder.lockId, // Defaults to FlowLogic.currentTopLevel?.runId?.uuid ?: UUID.randomUUID()
requiredAmount = requiredAmount,
queryBy = queryBy) // See section below on queries
// Exit states and get possible change output.
val (inputs, changeOutput) = generateExit(
exitStates = exitStates,
amount = requiredAmount,
changeHolder = changeHolder
)
// Call subflow to redeem states with the issuer
val issuerSession: FlowSession = ...
subflow(RedeemTokensFlow(inputs, changeOutput, issuerSession, observerSessions))
// or use utilities functions.
addTokensToRedeem(
transactionBuilder = transactionBuilder,
inputs = inputs,
changeOutput = changeOutput
)
Provide queries to LocalTokenSelector
You can provide additional queries to LocalTokenSelector
by constructing TokenQueryBy
and passing it to generateMove
or selectStates
methods.
TokenQueryBy
requires issuer to specify selection of token from given issuing party.
You can also provide any states filtering as predicate function.
val issuerParty: Party = ...
val notaryParty: Party = ...
// Get list of input and output states that can be passed to addMove or MoveTokensFlow
val (inputs, outputs) = localTokenSelector.generateMove(
partiesAndAmounts = listOf(Pair(receivingParty, tokensAmount)),
changeHolder = this.ourIdentity,
// Get tokens issued by issuerParty and notarised by notaryParty
queryBy = TokenQueryBy(issuer = issuerParty, predicate = { it.state.notary == notaryParty }))
Was this page helpful?
Thanks for your feedback!
Chat with us
Chat with us on our #docs channel on slack. You can also join a lot of other slack channels there and have access to 1-on-1 communication with members of the R3 team and the online community.
Propose documentation improvements directly
Help us to improve the docs by contributing directly. It's simple - just fork this repository and raise a PR of your own - R3's Technical Writers will review it and apply the relevant suggestions.
We're sorry this page wasn't helpful. Let us know how we can make it better!
Chat with us
Chat with us on our #docs channel on slack. You can also join a lot of other slack channels there and have access to 1-on-1 communication with members of the R3 team and the online community.
Create an issue
Create a new GitHub issue in this repository - submit technical feedback, draw attention to a potential documentation bug, or share ideas for improvement and general feedback.
Propose documentation improvements directly
Help us to improve the docs by contributing directly. It's simple - just fork this repository and raise a PR of your own - R3's Technical Writers will review it and apply the relevant suggestions.