mirror of
https://github.com/corda/corda.git
synced 2025-04-07 11:27:01 +00:00
Created some examples to include in tutorial on building transactions.
Complete the transaction building doc with code fragments. Fix gradle build Handle PR comments Put back in missing main class line Couple of minor improvements from PR
This commit is contained in:
parent
4012d4b136
commit
4a504ca3dc
@ -1,6 +1,7 @@
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'application'
|
||||
apply plugin: 'net.corda.plugins.cordformation'
|
||||
apply plugin: 'net.corda.plugins.quasar-utils'
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
@ -48,6 +49,8 @@ dependencies {
|
||||
runtime "net.corda:corda:$corda_version"
|
||||
}
|
||||
|
||||
mainClassName = "net.corda.docs.ClientRpcTutorialKt"
|
||||
|
||||
task getClientRpcTutorial(type: CreateStartScripts) {
|
||||
dependsOn(classes)
|
||||
mainClassName = "net.corda.docs.ClientRpcTutorialKt"
|
||||
|
@ -0,0 +1,271 @@
|
||||
package net.corda.docs
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.Issued
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.TransactionType
|
||||
import net.corda.core.crypto.DigitalSignature
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.signWithECDSA
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.node.PluginServiceHub
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.flows.FinalityFlow
|
||||
import net.corda.flows.ResolveTransactionsFlow
|
||||
import java.util.*
|
||||
|
||||
object FxTransactionDemoTutorial {
|
||||
// Would normally be called by custom service init in a CorDapp
|
||||
fun registerFxProtocols(pluginHub: PluginServiceHub) {
|
||||
pluginHub.registerFlowInitiator(ForeignExchangeFlow::class, ::ForeignExchangeRemoteFlow)
|
||||
}
|
||||
}
|
||||
|
||||
private data class FxRequest(val tradeId: String,
|
||||
val amount: Amount<Issued<Currency>>,
|
||||
val owner: Party,
|
||||
val counterparty: Party,
|
||||
val notary: Party? = null)
|
||||
|
||||
private data class FxResponse(val inputs: List<StateAndRef<Cash.State>>,
|
||||
val outputs: List<Cash.State>)
|
||||
|
||||
// DOCSTART 1
|
||||
// This is equivalent to the VaultService.generateSpend
|
||||
// Which is brought here to make the filtering logic more visible in the example
|
||||
private fun gatherOurInputs(serviceHub: ServiceHub,
|
||||
amountRequired: Amount<Issued<Currency>>,
|
||||
notary: Party?): Pair<List<StateAndRef<Cash.State>>, Long> {
|
||||
// Collect cash type inputs
|
||||
val cashStates = serviceHub.vaultService.currentVault.statesOfType<Cash.State>()
|
||||
// extract our key identity for convenience
|
||||
val ourKey = serviceHub.myInfo.legalIdentity.owningKey
|
||||
// Filter down to our own cash states with right currency and issuer
|
||||
val suitableCashStates = cashStates.filter {
|
||||
val state = it.state.data
|
||||
(state.owner == ourKey)
|
||||
&& (state.amount.token == amountRequired.token)
|
||||
}
|
||||
require(!suitableCashStates.isEmpty()) { "Insufficient funds" }
|
||||
var remaining = amountRequired.quantity
|
||||
// We will need all of the inputs to be on the same notary.
|
||||
// For simplicity we just filter on the first notary encountered
|
||||
// A production quality flow would need to migrate notary if the
|
||||
// the amounts were not sufficient in any one notary
|
||||
val sourceNotary: Party = notary ?: suitableCashStates.first().state.notary
|
||||
|
||||
val inputsList = mutableListOf<StateAndRef<Cash.State>>()
|
||||
// Iterate over filtered cash states to gather enough to pay
|
||||
for (cash in suitableCashStates.filter { it.state.notary == sourceNotary }) {
|
||||
inputsList += cash
|
||||
if (remaining <= cash.state.data.amount.quantity) {
|
||||
return Pair(inputsList, cash.state.data.amount.quantity - remaining)
|
||||
}
|
||||
remaining -= cash.state.data.amount.quantity
|
||||
}
|
||||
throw IllegalStateException("Insufficient funds")
|
||||
}
|
||||
// DOCEND 1
|
||||
|
||||
private fun prepareOurInputsAndOutputs(serviceHub: ServiceHub, request: FxRequest): FxResponse {
|
||||
// Create amount with correct issuer details
|
||||
val sellAmount = request.amount
|
||||
|
||||
// DOCSTART 2
|
||||
// Gather our inputs. We would normally use VaultService.generateSpend
|
||||
// to carry out the build in a single step. To be more explicit
|
||||
// we will use query manually in the helper function below.
|
||||
// Putting this into a non-suspendable function also prevents issues when
|
||||
// the flow is suspended.
|
||||
val (inputs, residual) = gatherOurInputs(serviceHub, sellAmount, request.notary)
|
||||
|
||||
// Build and an output state for the counterparty
|
||||
val transferedFundsOutput = Cash.State(sellAmount, request.counterparty.owningKey, null)
|
||||
|
||||
if (residual > 0L) {
|
||||
// Build an output state for the residual change back to us
|
||||
val residualAmount = Amount(residual, sellAmount.token)
|
||||
val residualOutput = Cash.State(residualAmount, serviceHub.myInfo.legalIdentity.owningKey, null)
|
||||
return FxResponse(inputs, listOf(transferedFundsOutput, residualOutput))
|
||||
} else {
|
||||
return FxResponse(inputs, listOf(transferedFundsOutput))
|
||||
}
|
||||
// DOCEND 2
|
||||
}
|
||||
|
||||
// A flow representing creating a transaction that
|
||||
// carries out exchange of cash assets.
|
||||
class ForeignExchangeFlow(val tradeId: String,
|
||||
val baseCurrencyAmount: Amount<Issued<Currency>>,
|
||||
val quoteCurrencyAmount: Amount<Issued<Currency>>,
|
||||
val baseCurrencyBuyer: Party,
|
||||
val baseCurrencySeller: Party) : FlowLogic<SecureHash>() {
|
||||
@Suspendable
|
||||
override fun call(): SecureHash {
|
||||
// Select correct sides of the Fx exchange to query for.
|
||||
// Specifically we own the assets we wish to sell.
|
||||
// Also prepare the other side query
|
||||
val (localRequest, remoteRequest) = if (baseCurrencySeller == serviceHub.myInfo.legalIdentity) {
|
||||
val local = FxRequest(tradeId, baseCurrencyAmount, baseCurrencySeller, baseCurrencyBuyer)
|
||||
val remote = FxRequest(tradeId, quoteCurrencyAmount, baseCurrencyBuyer, baseCurrencySeller)
|
||||
Pair(local, remote)
|
||||
} else if (baseCurrencyBuyer == serviceHub.myInfo.legalIdentity) {
|
||||
val local = FxRequest(tradeId, quoteCurrencyAmount, baseCurrencyBuyer, baseCurrencySeller)
|
||||
val remote = FxRequest(tradeId, baseCurrencyAmount, baseCurrencySeller, baseCurrencyBuyer)
|
||||
Pair(local, remote)
|
||||
} else throw IllegalArgumentException("Our identity must be one of the parties in the trade.")
|
||||
|
||||
// Call the helper method to identify suitable inputs and make the outputs
|
||||
val ourStates = prepareOurInputsAndOutputs(serviceHub, localRequest)
|
||||
|
||||
// identify the notary for our states
|
||||
val notary = ourStates.inputs.first().state.notary
|
||||
// ensure request to other side is for a consistent notary
|
||||
val remoteRequestWithNotary = remoteRequest.copy(notary = notary)
|
||||
|
||||
// Send the request to the counterparty to verify and call their version of prepareOurInputsAndOutputs
|
||||
// Then they can return their candidate states
|
||||
val theirStates = sendAndReceive<FxResponse>(remoteRequestWithNotary.owner, remoteRequestWithNotary).unwrap {
|
||||
require(it.inputs.all { it.state.notary == notary }) {
|
||||
"notary of remote states must be same as for our states"
|
||||
}
|
||||
require(it.inputs.all { it.state.data.owner == remoteRequestWithNotary.owner.owningKey }) {
|
||||
"The inputs are not owned by the correct counterparty"
|
||||
}
|
||||
require(it.inputs.all { it.state.data.amount.token == remoteRequestWithNotary.amount.token }) {
|
||||
"Inputs not of the correct currency"
|
||||
}
|
||||
require(it.outputs.all { it.amount.token == remoteRequestWithNotary.amount.token }) {
|
||||
"Outputs not of the correct currency"
|
||||
}
|
||||
require(it.inputs.map { it.state.data.amount.quantity }.sum()
|
||||
>= remoteRequestWithNotary.amount.quantity) {
|
||||
"the provided inputs don't provide sufficient funds"
|
||||
}
|
||||
require(it.outputs.filter { it.owner == serviceHub.myInfo.legalIdentity.owningKey }.
|
||||
map { it.amount.quantity }.sum() == remoteRequestWithNotary.amount.quantity) {
|
||||
"the provided outputs don't provide the request quantity"
|
||||
}
|
||||
// Download their inputs chains to validate that they are OK
|
||||
val dependencyTxIDs = it.inputs.map { it.ref.txhash }.toSet()
|
||||
subFlow(ResolveTransactionsFlow(dependencyTxIDs, remoteRequestWithNotary.owner))
|
||||
|
||||
it // return validated response
|
||||
}
|
||||
|
||||
// having collated the data create the full transaction.
|
||||
val signedTransaction = buildTradeProposal(ourStates, theirStates)
|
||||
|
||||
// pass transaction details to the counterparty to revalidate and confirm with a signature
|
||||
val allPartySignedTx = sendAndReceive<DigitalSignature.WithKey>(remoteRequestWithNotary.owner, signedTransaction).unwrap {
|
||||
val withNewSignature = signedTransaction + it
|
||||
// check all signatures are present except the notary
|
||||
withNewSignature.verifySignatures(withNewSignature.tx.notary!!.owningKey)
|
||||
|
||||
// This verifies that the transaction is contract-valid, even though it is missing signatures.
|
||||
// In a full solution there would be states tracking the trade request which
|
||||
// would be included in the transaction and enforce the amounts and tradeId
|
||||
withNewSignature.tx.toLedgerTransaction(serviceHub).verify()
|
||||
|
||||
withNewSignature // return the almost complete transaction
|
||||
}
|
||||
|
||||
// Initiate the standard protocol to notarise and distribute to the involved parties
|
||||
subFlow(FinalityFlow(allPartySignedTx, setOf(baseCurrencyBuyer, baseCurrencySeller)))
|
||||
|
||||
return allPartySignedTx.id
|
||||
}
|
||||
|
||||
// DOCSTART 3
|
||||
private fun buildTradeProposal(ourStates: FxResponse, theirStates: FxResponse): SignedTransaction {
|
||||
// This is the correct way to create a TransactionBuilder,
|
||||
// do not construct directly.
|
||||
// We also set the notary to match the input notary
|
||||
val builder = TransactionType.General.Builder(ourStates.inputs.first().state.notary)
|
||||
|
||||
// Add the move commands and key to indicate all the respective owners and need to sign
|
||||
val ourSigners = ourStates.inputs.map { it.state.data.owner }.toSet()
|
||||
val theirSigners = theirStates.inputs.map { it.state.data.owner }.toSet()
|
||||
builder.addCommand(Cash.Commands.Move(), (ourSigners + theirSigners).toList())
|
||||
|
||||
// Build and add the inputs and outputs
|
||||
builder.withItems(*ourStates.inputs.toTypedArray())
|
||||
builder.withItems(*theirStates.inputs.toTypedArray())
|
||||
builder.withItems(*ourStates.outputs.toTypedArray())
|
||||
builder.withItems(*theirStates.outputs.toTypedArray())
|
||||
|
||||
// We have already validated their response and trust our own data
|
||||
// so we can sign
|
||||
builder.signWith(serviceHub.legalIdentityKey)
|
||||
// create a signed transaction, but pass false as parameter, because we know it is not fully signed
|
||||
val signedTransaction = builder.toSignedTransaction(checkSufficientSignatures = false)
|
||||
return signedTransaction
|
||||
}
|
||||
// DOCEND 3
|
||||
}
|
||||
|
||||
class ForeignExchangeRemoteFlow(val source: Party) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
// Initial receive from remote party
|
||||
val request = receive<FxRequest>(source).unwrap {
|
||||
// We would need to check that this is a known trade ID here!
|
||||
// Also that the amounts and source are correct with the trade details.
|
||||
// In a production system there would be other Corda contracts tracking
|
||||
// the lifecycle of the Fx trades which would be included in the transaction
|
||||
|
||||
// Check request is for us
|
||||
require(serviceHub.myInfo.legalIdentity == it.owner) {
|
||||
"Request does not include the correct counterparty"
|
||||
}
|
||||
require(source == it.counterparty) {
|
||||
"Request does not include the correct counterparty"
|
||||
}
|
||||
it // return validated request
|
||||
}
|
||||
|
||||
// Gather our inputs. We would normally use VaultService.generateSpend
|
||||
// to carry out the build in a single step. To be more explicit
|
||||
// we will use query manually in the helper function below.
|
||||
// Putting this into a non-suspendable function also prevent issues when
|
||||
// the flow is suspended.
|
||||
val ourResponse = prepareOurInputsAndOutputs(serviceHub, request)
|
||||
|
||||
// Send back our proposed states and await the full transaction to verify
|
||||
val proposedTrade = sendAndReceive<SignedTransaction>(source, ourResponse).unwrap {
|
||||
val wtx = it.tx
|
||||
// check all signatures are present except our own and the notary
|
||||
it.verifySignatures(serviceHub.myInfo.legalIdentity.owningKey, wtx.notary!!.owningKey)
|
||||
|
||||
// We need to fetch their complete input states and dependencies so that verify can operate
|
||||
checkDependencies(it)
|
||||
|
||||
// This verifies that the transaction is contract-valid, even though it is missing signatures.
|
||||
// In a full solution there would be states tracking the trade request which
|
||||
// would be included in the transaction and enforce the amounts and tradeId
|
||||
wtx.toLedgerTransaction(serviceHub).verify()
|
||||
|
||||
it // return the SignedTransaction
|
||||
}
|
||||
|
||||
// assuming we have completed state and business level validation we can sign the trade
|
||||
val ourSignature = serviceHub.legalIdentityKey.signWithECDSA(proposedTrade.id)
|
||||
|
||||
// send the other side our signature.
|
||||
send(source, ourSignature)
|
||||
// N.B. The FinalityProtocol will be responsible for Notarising the SignedTransaction
|
||||
// and broadcasting the result to us.
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun checkDependencies(stx: SignedTransaction) {
|
||||
// Download and check all the transactions that this transaction depends on, but do not check this
|
||||
// transaction itself.
|
||||
val dependencyTxIDs = stx.tx.inputs.map { it.txhash }.toSet()
|
||||
subFlow(ResolveTransactionsFlow(dependencyTxIDs, source))
|
||||
}
|
||||
}
|
@ -0,0 +1,265 @@
|
||||
package net.corda.docs
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.node.PluginServiceHub
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.services.linearHeadsOfType
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.flows.FinalityFlow
|
||||
import java.security.PublicKey
|
||||
import java.time.Duration
|
||||
|
||||
object WorkflowTransactionBuildTutorial {
|
||||
// Would normally be called by custom service init in a CorDapp
|
||||
fun registerWorkflowProtocols(pluginHub: PluginServiceHub) {
|
||||
pluginHub.registerFlowInitiator(SubmitCompletionFlow::class, ::RecordCompletionFlow)
|
||||
}
|
||||
}
|
||||
|
||||
// DOCSTART 1
|
||||
// Helper method to access the StorageService and expand a StateRef into a StateAndRef
|
||||
fun <T : ContractState> ServiceHub.toStateAndRef(ref: StateRef): StateAndRef<T> {
|
||||
return storageService.validatedTransactions.getTransaction(ref.txhash)!!.tx.outRef<T>(ref.index)
|
||||
}
|
||||
|
||||
// Helper method to locate the latest Vault version of a LinearState from a possibly out of date StateRef
|
||||
inline fun <reified T : LinearState> ServiceHub.latest(ref: StateRef): StateAndRef<T> {
|
||||
val linearHeads = vaultService.linearHeadsOfType<T>()
|
||||
val original = toStateAndRef<T>(ref)
|
||||
return linearHeads.get(original.state.data.linearId)!!
|
||||
}
|
||||
// DOCEND 1
|
||||
|
||||
// Minimal state model of a manual approval process
|
||||
enum class WorkflowState {
|
||||
NEW,
|
||||
APPROVED,
|
||||
REJECTED
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal contract to encode a simple workflow with one initial state and two possible eventual states.
|
||||
* It is assumed one party unilaterally submits and the other manually retrieves the deal and completes it.
|
||||
*/
|
||||
data class TradeApprovalContract(override val legalContractReference: SecureHash = SecureHash.sha256("Example of workflow type transaction")) : Contract {
|
||||
|
||||
interface Commands : CommandData {
|
||||
class Issue : TypeOnlyCommandData(), Commands // Record receipt of deal details
|
||||
class Completed : TypeOnlyCommandData(), Commands // Record match
|
||||
}
|
||||
|
||||
/**
|
||||
* Truly minimal state that just records a tradeId string and the parties involved.
|
||||
*/
|
||||
data class State(val tradeId: String,
|
||||
val source: Party,
|
||||
val counterparty: Party,
|
||||
val state: WorkflowState = WorkflowState.NEW,
|
||||
override val linearId: UniqueIdentifier = UniqueIdentifier(tradeId),
|
||||
override val contract: TradeApprovalContract = TradeApprovalContract()) : LinearState {
|
||||
|
||||
val parties: List<Party> get() = listOf(source, counterparty)
|
||||
override val participants: List<CompositeKey> get() = parties.map { it.owningKey }
|
||||
|
||||
override fun isRelevant(ourKeys: Set<PublicKey>): Boolean {
|
||||
return participants.any { it.containsAny(ourKeys) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The verify method locks down the allowed transactions to contain just a single proposal being
|
||||
* created/modified and the only modification allowed is to the state field.
|
||||
*/
|
||||
override fun verify(tx: TransactionForContract) {
|
||||
val command = tx.commands.requireSingleCommand<TradeApprovalContract.Commands>()
|
||||
require(tx.timestamp?.midpoint != null) { "must be timestamped" }
|
||||
when (command.value) {
|
||||
is Commands.Issue -> {
|
||||
requireThat {
|
||||
"Issue of new WorkflowContract must not include any inputs" by (tx.inputs.isEmpty())
|
||||
"Issue of new WorkflowContract must be in a unique transaction" by (tx.outputs.size == 1)
|
||||
}
|
||||
val issued = tx.outputs.get(0) as TradeApprovalContract.State
|
||||
requireThat {
|
||||
"Issue requires the source Party as signer" by (command.signers.contains(issued.source.owningKey))
|
||||
"Initial Issue state must be NEW" by (issued.state == WorkflowState.NEW)
|
||||
}
|
||||
}
|
||||
is Commands.Completed -> {
|
||||
val stateGroups = tx.groupStates(TradeApprovalContract.State::class.java) { it.linearId }
|
||||
require(stateGroups.size == 1) { "Must be only a single proposal in transaction" }
|
||||
for (group in stateGroups) {
|
||||
val before = group.inputs.single()
|
||||
val after = group.outputs.single()
|
||||
requireThat {
|
||||
"Only a non-final trade can be modified" by (before.state == WorkflowState.NEW)
|
||||
"Output must be a final state" by (after.state in setOf(WorkflowState.APPROVED, WorkflowState.REJECTED))
|
||||
"Completed command can only change state" by (before == after.copy(state = before.state))
|
||||
"Completed command requires the source Party as signer" by (command.signers.contains(before.source.owningKey))
|
||||
"Completed command requires the counterparty as signer" by (command.signers.contains(before.counterparty.owningKey))
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> throw IllegalArgumentException("Unrecognised Command $command")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple flow to create a workflow state, sign and notarise it.
|
||||
* The protocol then sends a copy to the other node. We don't require the other party to sign
|
||||
* as their approval/rejection is to follow.
|
||||
*/
|
||||
class SubmitTradeApprovalFlow(val tradeId: String,
|
||||
val counterparty: Party) : FlowLogic<StateAndRef<TradeApprovalContract.State>>() {
|
||||
@Suspendable
|
||||
override fun call(): StateAndRef<TradeApprovalContract.State> {
|
||||
// Manufacture an initial state
|
||||
val tradeProposal = TradeApprovalContract.State(
|
||||
tradeId,
|
||||
serviceHub.myInfo.legalIdentity,
|
||||
counterparty)
|
||||
// identify a notary. This might also be done external to the flow
|
||||
val notary = serviceHub.networkMapCache.getAnyNotary()
|
||||
// Create the TransactionBuilder and populate with the new state.
|
||||
val tx = TransactionType.
|
||||
General.
|
||||
Builder(notary).
|
||||
withItems(tradeProposal,
|
||||
Command(TradeApprovalContract.Commands.Issue(),
|
||||
listOf(tradeProposal.source.owningKey)))
|
||||
tx.setTime(serviceHub.clock.instant(), Duration.ofSeconds(60))
|
||||
// We can automatically sign as there is no untrusted data.
|
||||
tx.signWith(serviceHub.legalIdentityKey)
|
||||
// Convert to a SignedTransaction that we can send to the notary
|
||||
val signedTx = tx.toSignedTransaction(false)
|
||||
// Run the FinalityFlow to notarise and distribute the SignedTransaction to the counterparty
|
||||
subFlow(FinalityFlow(signedTx, setOf(serviceHub.myInfo.legalIdentity, counterparty)))
|
||||
// Return the initial state
|
||||
return signedTx.tx.outRef<TradeApprovalContract.State>(0)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple flow to complete a proposal submitted by another party and ensure both nodes
|
||||
* end up with a fully signed copy of the state either as APPROVED, or REJECTED
|
||||
*/
|
||||
class SubmitCompletionFlow(val ref: StateRef, val verdict: WorkflowState) : FlowLogic<StateAndRef<TradeApprovalContract.State>>() {
|
||||
init {
|
||||
require(verdict in setOf(WorkflowState.APPROVED, WorkflowState.REJECTED)) {
|
||||
"Verdict must be a final state"
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call(): StateAndRef<TradeApprovalContract.State> {
|
||||
// Pull in the latest Vault version of the StateRef as a full StateAndRef
|
||||
val latestRecord = serviceHub.latest<TradeApprovalContract.State>(ref)
|
||||
// Check the protocol hasn't already been run
|
||||
require(latestRecord.ref == ref) {
|
||||
"Input trade $ref is not latest version $latestRecord"
|
||||
}
|
||||
// Require that the state is still modifiable
|
||||
require(latestRecord.state.data.state == WorkflowState.NEW) {
|
||||
"Input trade not modifiable ${latestRecord.state.data.state}"
|
||||
}
|
||||
// Check we are the correct Party to run the protocol. Note they will counter check this too.
|
||||
require(latestRecord.state.data.counterparty == serviceHub.myInfo.legalIdentity) {
|
||||
"The counterparty must give the verdict"
|
||||
}
|
||||
|
||||
// DOCSTART 2
|
||||
// Modify the state field for new output. We use copy, to ensure no other modifications.
|
||||
// It is especially important for a LinearState that the linearId is copied across,
|
||||
// not accidentally assigned a new random id.
|
||||
val newState = latestRecord.state.data.copy(state = verdict)
|
||||
|
||||
// We have to use the original notary for the new transaction
|
||||
val notary = latestRecord.state.notary
|
||||
|
||||
// Get and populate the new TransactionBuilder
|
||||
// To destroy the old proposal state and replace with the new completion state.
|
||||
// Also add the Completed command with keys of all parties to signal the Tx purpose
|
||||
// to the Contract verify method.
|
||||
val tx = TransactionType.
|
||||
General.
|
||||
Builder(notary).
|
||||
withItems(
|
||||
latestRecord,
|
||||
newState,
|
||||
Command(TradeApprovalContract.Commands.Completed(),
|
||||
listOf(serviceHub.myInfo.legalIdentity.owningKey, latestRecord.state.data.source.owningKey)))
|
||||
tx.setTime(serviceHub.clock.instant(), Duration.ofSeconds(60))
|
||||
// We can sign this transaction immediately as we have already checked all the fields and the decision
|
||||
// is ultimately a manual one from the caller.
|
||||
tx.signWith(serviceHub.legalIdentityKey)
|
||||
// Convert to SignedTransaction we can pass around certain that it cannot be modified.
|
||||
val selfSignedTx = tx.toSignedTransaction(false)
|
||||
//DOCEND 2
|
||||
// Send the signed transaction to the originator and await their signature to confirm
|
||||
val allPartySignedTx = sendAndReceive<DigitalSignature.WithKey>(newState.source, selfSignedTx).unwrap {
|
||||
// Add their signature to our unmodified transaction. To check they signed the same tx.
|
||||
val agreedTx = selfSignedTx + it
|
||||
// Receive back their signature and confirm that it is for an unmodified transaction
|
||||
// Also that the only missing signature is from teh Notary
|
||||
agreedTx.verifySignatures(notary.owningKey)
|
||||
// Recheck the data of the transaction. Note we run toLedgerTransaction on the WireTransaction
|
||||
// as we do not have all the signature.
|
||||
agreedTx.tx.toLedgerTransaction(serviceHub).verify()
|
||||
// return the SignedTransaction to notarise
|
||||
agreedTx
|
||||
}
|
||||
// DOCSTART 4
|
||||
// Run the FinalityFlow to notarise and distribute the completed transaction.
|
||||
subFlow(FinalityFlow(allPartySignedTx,
|
||||
setOf(latestRecord.state.data.source, latestRecord.state.data.counterparty)))
|
||||
|
||||
// DOCEND 4
|
||||
// Return back the details of the completed state/transaction.
|
||||
return allPartySignedTx.tx.outRef<TradeApprovalContract.State>(0)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple flow to receive the final decision on a proposal.
|
||||
* Then after checking to sign it and eventually store the fully notarised
|
||||
* transaction to the ledger.
|
||||
*/
|
||||
class RecordCompletionFlow(val source: Party) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call(): Unit {
|
||||
// DOCSTART 3
|
||||
// First we receive the verdict transaction signed by their single key
|
||||
val completeTx = receive<SignedTransaction>(source).unwrap {
|
||||
// Check the transaction is signed apart from our own key and the notary
|
||||
val wtx = it.verifySignatures(serviceHub.myInfo.legalIdentity.owningKey, it.tx.notary!!.owningKey)
|
||||
// Check the transaction data is correctly formed
|
||||
wtx.toLedgerTransaction(serviceHub).verify()
|
||||
// Confirm that this is the expected type of transaction
|
||||
require(wtx.commands.single().value is TradeApprovalContract.Commands.Completed) {
|
||||
"Transaction must represent a workflow completion"
|
||||
}
|
||||
// Check the context dependent parts of the transaction as the
|
||||
// Contract verify method must not use serviceHub queries.
|
||||
val state = wtx.outRef<TradeApprovalContract.State>(0)
|
||||
require(state.state.data.source == serviceHub.myInfo.legalIdentity) {
|
||||
"Proposal not one of our original proposals"
|
||||
}
|
||||
require(state.state.data.counterparty == source) {
|
||||
"Proposal not for sent from correct source"
|
||||
}
|
||||
it
|
||||
}
|
||||
// DOCEND 3
|
||||
// Having verified the SignedTransaction passed to us we can sign it too
|
||||
val ourSignature = serviceHub.legalIdentityKey.signWithECDSA(completeTx.tx.id)
|
||||
// Send our signature to the other party.
|
||||
send(source, ourSignature)
|
||||
// N.B. The FinalityProtocol will be responsible for Notarising the SignedTransaction
|
||||
// and broadcasting the result to us.
|
||||
}
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
package net.corda.docs
|
||||
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.getOrThrow
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.core.node.services.linearHeadsOfType
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.utilities.DUMMY_NOTARY
|
||||
import net.corda.core.utilities.DUMMY_NOTARY_KEY
|
||||
import net.corda.flows.CashCommand
|
||||
import net.corda.flows.CashFlow
|
||||
import net.corda.node.services.network.NetworkMapService
|
||||
import net.corda.node.services.transactions.ValidatingNotaryService
|
||||
import net.corda.node.utilities.databaseTransaction
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class FxTransactionBuildTutorialTest {
|
||||
lateinit var net: MockNetwork
|
||||
lateinit var notaryNode: MockNetwork.MockNode
|
||||
lateinit var nodeA: MockNetwork.MockNode
|
||||
lateinit var nodeB: MockNetwork.MockNode
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
net = MockNetwork(threadPerNode = true)
|
||||
notaryNode = net.createNode(
|
||||
legalName = DUMMY_NOTARY.name,
|
||||
keyPair = DUMMY_NOTARY_KEY,
|
||||
advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(ValidatingNotaryService.type)))
|
||||
nodeA = net.createPartyNode(notaryNode.info.address)
|
||||
nodeB = net.createPartyNode(notaryNode.info.address)
|
||||
FxTransactionDemoTutorial.registerFxProtocols(nodeA.services)
|
||||
FxTransactionDemoTutorial.registerFxProtocols(nodeB.services)
|
||||
WorkflowTransactionBuildTutorial.registerWorkflowProtocols(nodeA.services)
|
||||
WorkflowTransactionBuildTutorial.registerWorkflowProtocols(nodeB.services)
|
||||
}
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
println("Close DB")
|
||||
net.stopNodes()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Run ForeignExchangeFlow to completion`() {
|
||||
// Use NodeA as issuer and create some dollars
|
||||
val flowHandle1 = nodeA.services.startFlow(CashFlow(CashCommand.IssueCash(DOLLARS(1000),
|
||||
OpaqueBytes.of(0x01),
|
||||
nodeA.info.legalIdentity,
|
||||
notaryNode.info.notaryIdentity)))
|
||||
// Wait for the flow to stop and print
|
||||
flowHandle1.resultFuture.getOrThrow()
|
||||
printBalances()
|
||||
|
||||
// Using NodeB as Issuer create some pounds.
|
||||
val flowHandle2 = nodeB.services.startFlow(CashFlow(CashCommand.IssueCash(POUNDS(1000),
|
||||
OpaqueBytes.of(0x01),
|
||||
nodeB.info.legalIdentity,
|
||||
notaryNode.info.notaryIdentity)))
|
||||
// Wait for flow to come to an end and print
|
||||
flowHandle2.resultFuture.getOrThrow()
|
||||
printBalances()
|
||||
|
||||
// Setup some futures on the vaults to await the arrival of the exchanged funds at both nodes
|
||||
val done2 = SettableFuture.create<Unit>()
|
||||
val done3 = SettableFuture.create<Unit>()
|
||||
val subs2 = nodeA.services.vaultService.updates.subscribe {
|
||||
done2.set(Unit)
|
||||
}
|
||||
val subs3 = nodeB.services.vaultService.updates.subscribe {
|
||||
done3.set(Unit)
|
||||
}
|
||||
// Now run the actual Fx exchange
|
||||
val doIt = nodeA.services.startFlow(ForeignExchangeFlow("trade1",
|
||||
POUNDS(100).issuedBy(nodeB.info.legalIdentity.ref(0x01)),
|
||||
DOLLARS(200).issuedBy(nodeA.info.legalIdentity.ref(0x01)),
|
||||
nodeA.info.legalIdentity,
|
||||
nodeB.info.legalIdentity))
|
||||
// wait for the flow to finish and the vault updates to be done
|
||||
doIt.resultFuture.getOrThrow()
|
||||
done2.get()
|
||||
done3.get()
|
||||
subs2.unsubscribe()
|
||||
subs3.unsubscribe()
|
||||
// Check the final balances
|
||||
val balancesA = databaseTransaction(nodeA.database) {
|
||||
nodeA.services.vaultService.cashBalances
|
||||
}
|
||||
val balancesB = databaseTransaction(nodeB.database) {
|
||||
nodeB.services.vaultService.cashBalances
|
||||
}
|
||||
println("BalanceA\n" + balancesA)
|
||||
println("BalanceB\n" + balancesB)
|
||||
// Verify the transfers occurred as expected
|
||||
assertEquals(POUNDS(100), balancesA[GBP])
|
||||
assertEquals(DOLLARS(1000 - 200), balancesA[USD])
|
||||
assertEquals(POUNDS(1000 - 100), balancesB[GBP])
|
||||
assertEquals(DOLLARS(200), balancesB[USD])
|
||||
}
|
||||
|
||||
private fun printBalances() {
|
||||
// Print out the balances
|
||||
databaseTransaction(nodeA.database) {
|
||||
println("BalanceA\n" + nodeA.services.vaultService.cashBalances)
|
||||
}
|
||||
databaseTransaction(nodeB.database) {
|
||||
println("BalanceB\n" + nodeB.services.vaultService.cashBalances)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,117 @@
|
||||
package net.corda.docs
|
||||
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import net.corda.core.contracts.LinearState
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.getOrThrow
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.core.node.services.linearHeadsOfType
|
||||
import net.corda.core.utilities.DUMMY_NOTARY
|
||||
import net.corda.core.utilities.DUMMY_NOTARY_KEY
|
||||
import net.corda.node.services.network.NetworkMapService
|
||||
import net.corda.node.services.transactions.ValidatingNotaryService
|
||||
import net.corda.node.utilities.databaseTransaction
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class WorkflowTransactionBuildTutorialTest {
|
||||
lateinit var net: MockNetwork
|
||||
lateinit var notaryNode: MockNetwork.MockNode
|
||||
lateinit var nodeA: MockNetwork.MockNode
|
||||
lateinit var nodeB: MockNetwork.MockNode
|
||||
|
||||
// Helper method to locate the latest Vault version of a LinearState from a possibly out of date StateRef
|
||||
private inline fun <reified T : LinearState> ServiceHub.latest(ref: StateRef): StateAndRef<T> {
|
||||
val linearHeads = vaultService.linearHeadsOfType<T>()
|
||||
val original = storageService.validatedTransactions.getTransaction(ref.txhash)!!.tx.outRef<T>(ref.index)
|
||||
return linearHeads.get(original.state.data.linearId)!!
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
net = MockNetwork(threadPerNode = true)
|
||||
notaryNode = net.createNode(
|
||||
legalName = DUMMY_NOTARY.name,
|
||||
keyPair = DUMMY_NOTARY_KEY,
|
||||
advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(ValidatingNotaryService.type)))
|
||||
nodeA = net.createPartyNode(notaryNode.info.address)
|
||||
nodeB = net.createPartyNode(notaryNode.info.address)
|
||||
FxTransactionDemoTutorial.registerFxProtocols(nodeA.services)
|
||||
FxTransactionDemoTutorial.registerFxProtocols(nodeB.services)
|
||||
WorkflowTransactionBuildTutorial.registerWorkflowProtocols(nodeA.services)
|
||||
WorkflowTransactionBuildTutorial.registerWorkflowProtocols(nodeB.services)
|
||||
}
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
println("Close DB")
|
||||
net.stopNodes()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Run workflow to completion`() {
|
||||
// Setup a vault subscriber to wait for successful upload of the proposal to NodeB
|
||||
val done1 = SettableFuture.create<Unit>()
|
||||
val subs1 = nodeB.services.vaultService.updates.subscribe {
|
||||
done1.set(Unit)
|
||||
}
|
||||
// Kick of the proposal flow
|
||||
val flow1 = nodeA.services.startFlow(SubmitTradeApprovalFlow("1234", nodeB.info.legalIdentity))
|
||||
// Wait for the flow to finish
|
||||
val proposalRef = flow1.resultFuture.getOrThrow()
|
||||
// Wait for NodeB to include it's copy in the vault
|
||||
done1.get()
|
||||
subs1.unsubscribe()
|
||||
// Fetch the latest copy of the state from both nodes
|
||||
val latestFromA = databaseTransaction(nodeA.database) {
|
||||
nodeA.services.latest<TradeApprovalContract.State>(proposalRef.ref)
|
||||
}
|
||||
val latestFromB = databaseTransaction(nodeB.database) {
|
||||
nodeB.services.latest<TradeApprovalContract.State>(proposalRef.ref)
|
||||
}
|
||||
// Confirm the state as as expected
|
||||
assertEquals(WorkflowState.NEW, proposalRef.state.data.state)
|
||||
assertEquals("1234", proposalRef.state.data.tradeId)
|
||||
assertEquals(nodeA.info.legalIdentity, proposalRef.state.data.source)
|
||||
assertEquals(nodeB.info.legalIdentity, proposalRef.state.data.counterparty)
|
||||
assertEquals(proposalRef, latestFromA)
|
||||
assertEquals(proposalRef, latestFromB)
|
||||
// Setup a vault subscriber to pause until the final update is in NodeA and NodeB
|
||||
val done2 = SettableFuture.create<Unit>()
|
||||
val subs2 = nodeA.services.vaultService.updates.subscribe {
|
||||
done2.set(Unit)
|
||||
}
|
||||
val done3 = SettableFuture.create<Unit>()
|
||||
val subs3 = nodeB.services.vaultService.updates.subscribe {
|
||||
done3.set(Unit)
|
||||
}
|
||||
// Run the manual completion flow from NodeB
|
||||
val flow2 = nodeB.services.startFlow(SubmitCompletionFlow(latestFromB.ref, WorkflowState.APPROVED))
|
||||
// wait for the flow to end
|
||||
val completedRef = flow2.resultFuture.getOrThrow()
|
||||
// wait for the vault updates to stabilise
|
||||
done2.get()
|
||||
done3.get()
|
||||
subs2.unsubscribe()
|
||||
subs3.unsubscribe()
|
||||
// Fetch the latest copies from the vault
|
||||
val finalFromA = databaseTransaction(nodeA.database) {
|
||||
nodeA.services.latest<TradeApprovalContract.State>(proposalRef.ref)
|
||||
}
|
||||
val finalFromB = databaseTransaction(nodeB.database) {
|
||||
nodeB.services.latest<TradeApprovalContract.State>(proposalRef.ref)
|
||||
}
|
||||
// Confirm the state is as expected
|
||||
assertEquals(WorkflowState.APPROVED, completedRef.state.data.state)
|
||||
assertEquals("1234", completedRef.state.data.tradeId)
|
||||
assertEquals(nodeA.info.legalIdentity, completedRef.state.data.source)
|
||||
assertEquals(nodeB.info.legalIdentity, completedRef.state.data.counterparty)
|
||||
assertEquals(completedRef, finalFromA)
|
||||
assertEquals(completedRef, finalFromB)
|
||||
}
|
||||
}
|
@ -72,6 +72,7 @@ Read on to learn:
|
||||
tutorial-test-dsl
|
||||
tutorial-integration-testing
|
||||
tutorial-clientrpc-api
|
||||
tutorial-building-transactions
|
||||
flow-state-machines
|
||||
flow-testing
|
||||
running-a-notary
|
||||
|
321
docs/source/tutorial-building-transactions.rst
Normal file
321
docs/source/tutorial-building-transactions.rst
Normal file
@ -0,0 +1,321 @@
|
||||
Building Transactions
|
||||
=====================
|
||||
|
||||
Introduction
|
||||
------------
|
||||
|
||||
Understanding and implementing transactions in Corda is key to building
|
||||
and implementing real world smart contracts. It is only through
|
||||
construction of valid Corda transactions containing appropriate data
|
||||
that nodes on the ledger can map real world business objects into a
|
||||
shared digital view of the data in the Corda ledger. More importantly as
|
||||
the developer of new smart contracts it is the code which determines
|
||||
what data is well formed and what data should be rejected as mistakes,
|
||||
or to prevent malicious activity. This document details some of the
|
||||
considerations and APIs used to when constructing transactions as part
|
||||
of a flow.
|
||||
|
||||
The Basic Lifecycle Of Transactions
|
||||
-----------------------------------
|
||||
|
||||
Transactions in Corda are constructed in stages and contain a number of
|
||||
elements. In particular a transaction’s core data structure is the
|
||||
``net.corda.core.transactions.WireTransaction``, which is usually
|
||||
manipulated via a
|
||||
``net.corda.core.contracts.General.TransactionBuilder`` and contains:
|
||||
|
||||
1. A set of Input state references that will be consumed by the final
|
||||
accepted transaction.
|
||||
|
||||
2. A set of Output states to create/replace the consumed states and thus
|
||||
become the new latest versions of data on the ledger.
|
||||
|
||||
3. A set of ``Attachment`` items which can contain legal documents, contract
|
||||
code, or private encrypted sections as an extension beyond the native
|
||||
contract states.
|
||||
|
||||
4. A set of ``Command`` items which give a context to the type of ledger
|
||||
transition that is encoded in the transaction. Also each command has an
|
||||
associated set of signer keys, which will be required to sign the
|
||||
transaction.
|
||||
|
||||
5. A signers list, which is populated by the ``TransactionBuilder`` to
|
||||
be the union of the signers on the individual Command objects.
|
||||
|
||||
6. A notary identity to specify the Notary node which is tracking the
|
||||
state consumption. (If the input states are registered with different
|
||||
notary nodes the flow will have to insert additional ``NotaryChange``
|
||||
transactions to migrate the states across to a consistent notary node,
|
||||
before being allowed to mutate any states.)
|
||||
|
||||
7. Optionally a timestamp that can used in the Notary to time bound the
|
||||
period in which the proposed transaction stays valid.
|
||||
|
||||
Typically, the ``WireTransaction`` should be regarded as a proposal and
|
||||
may need to be exchanged back and forth between parties before it can be
|
||||
fully populated. This is an immediate consequence of the Corda privacy
|
||||
model, which means that the input states are likely to be unknown to the
|
||||
other node.
|
||||
|
||||
Once the proposed data is fully populated the flow code should freeze
|
||||
the ``WireTransaction`` and form a ``SignedTransaction``. This is key to
|
||||
the ledger agreement process, as once a flow has attached a node’s
|
||||
signature it has stated that all details of the transaction are
|
||||
acceptable to it. A flow should take care not to attach signatures to
|
||||
intermediate data, which might be maliciously used to construct a
|
||||
different ``SignedTransaction``. For instance in a foreign exchange
|
||||
scenario we shouldn't send a ``SignedTransaction`` with only our sell
|
||||
side populated as that could be used to take the money without the
|
||||
expected return of the other currency. Also, it is best practice for
|
||||
flows to receive back the ``DigitalSignature.WithKey`` of other parties
|
||||
rather than a full ``SignedTransaction`` objects, because otherwise we
|
||||
have to separately check that this is still the same
|
||||
``SignedTransaction`` and not a malicious substitute.
|
||||
|
||||
The final stage of committing the transaction to the ledger is to
|
||||
notarise the ``SignedTransaction``, distribute this to all appropriate
|
||||
parties and record the data into the ledger. These actions are best
|
||||
delegated to the ``FinalityFlow``, rather than calling the inidividual
|
||||
steps manually. However, do note that the final broadcast to the other
|
||||
nodes is asynchronous, so care must be used in unit testing to
|
||||
correctly await the Vault updates.
|
||||
|
||||
Gathering Inputs
|
||||
----------------
|
||||
|
||||
One of the first steps to forming a transaction is gathering the set of
|
||||
input references. This process will clearly vary according to the nature
|
||||
of the business process being captured by the smart contract and the
|
||||
parameterised details of the request. However, it will generally involve
|
||||
searching the Vault via the ``VaultService`` interface on the
|
||||
``ServiceHub`` to locate the input states.
|
||||
|
||||
To give a few more specific details consider two simplified real world
|
||||
scenarios. First, a basic foreign exchange Cash transaction. This
|
||||
transaction needs to locate a set of funds to exchange. A flow
|
||||
modelling this is implemented in ``FxTransactionBuildTutorial.kt``.
|
||||
Second, a simple business model in which parties manually accept, or
|
||||
reject each other's trade proposals which is implemented in
|
||||
``WorkflowTransactionBuildTutorial.kt``. To run and explore these
|
||||
examples using the IntelliJ IDE one can run/step the respective unit
|
||||
tests in ``FxTransactionBuildTutorialTest.kt`` and
|
||||
``WorkflowTransactionBuildTutorialTest.kt``, which drive the flows as
|
||||
part of a simulated in-memory network of nodes. When creating the
|
||||
IntelliJ run configuration for these unit test set the workspace
|
||||
points to the root ``r3prototyping`` folder and add
|
||||
``-javaagent:lib/quasar.jar`` to the VM options, so that the ``Quasar``
|
||||
instrumentation is correctly configured.
|
||||
|
||||
For the Cash transaction let’s assume the cash resources are using the
|
||||
standard ``CashState`` in the ``:financial`` Gradle module. The Cash
|
||||
contract uses ``FungibleAsset`` states to model holdings of
|
||||
interchangeable assets and allow the split/merge and summing of
|
||||
states to meet a contractual obligation. We would normally use the
|
||||
``generateSpend`` method on the ``VaultService`` to gather the required
|
||||
amount of cash into a ``TransactionBuilder``, set the outputs and move
|
||||
command. However, to elucidate more clearly example flow code is shown
|
||||
here that will manually carry out the inputs queries using the lower
|
||||
level ``VaultService``.
|
||||
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 1
|
||||
:end-before: DOCEND 1
|
||||
|
||||
As a foreign exchange transaction we expect an exchange of two
|
||||
currencies, so we will also require a set of input states from the other
|
||||
counterparty. However, the Corda privacy model means we do not know the
|
||||
other node’s states. Our flow must therefore negotiate with the other
|
||||
node for them to carry out a similar query and populate the inputs (See
|
||||
the ``ForeignExchangeFlow`` for more details of the exchange). Having
|
||||
identified a set of Input ``StateRef`` items we can then create the
|
||||
output as discussed below.
|
||||
|
||||
For the trade approval flow we need to implement a simple workflow
|
||||
pattern. We start by recording the unconfirmed trade details in a state
|
||||
object implementing the ``LinearState`` interface. One field of this
|
||||
record is used to map the business workflow to an enumerated state.
|
||||
Initially the initiator creates a new state object which receives a new
|
||||
``UniqueIdentifier`` in its ``linearId`` property and a starting
|
||||
workflow state of ``NEW``. The ``Contract.verify`` method is written to
|
||||
allow the initiator to sign this initial transaction and send it to the
|
||||
other party. This pattern ensures that a permanent copy is recorded on
|
||||
both ledgers for audit purposes, but the state is prevented from being
|
||||
maliciously put in an approved state. The subsequent workflow steps then
|
||||
follow with transactions that consume the state as inputs on one side
|
||||
and output a new version with whatever state updates, or amendments
|
||||
match to the business process, the ``linearId`` being preserved across
|
||||
the changes. Attached ``Command`` objects help the verify method
|
||||
restrict changes to appropriate fields and signers at each step in the
|
||||
workflow. In this it is typical to have both parties sign the change
|
||||
transactions, but it can be valid to allow unilateral signing, if for instance
|
||||
one side could block a rejection. Commonly the manual initiator of these
|
||||
workflows will query the Vault for states of the right contract type and
|
||||
in the right workflow state over the RPC interface. The RPC will then
|
||||
initiate the relevant flow using ``StateRef``, or ``linearId`` values as
|
||||
parameters to the flow to identify the states being operated upon. Thus
|
||||
code to gather the latest input state would be:
|
||||
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 1
|
||||
:end-before: DOCEND 1
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
// Pull in the latest Vault version of the StateRef as a full StateAndRef
|
||||
val latestRecord = serviceHub.latest<TradeApprovalContract.State>(ref)
|
||||
|
||||
|
||||
Generating Commands
|
||||
-------------------
|
||||
|
||||
For the commands that will be added to the transaction, these will need
|
||||
to correctly reflect the task at hand. These must match because inside
|
||||
the ``Contract.verify`` method the command will be used to select the
|
||||
validation code path. The ``Contract.verify`` method will then restrict
|
||||
the allowed contents of the transaction to reflect this context. Typical
|
||||
restrictions might include that the input cash amount must equal the
|
||||
output cash amount, or that a workflow step is only allowed to change
|
||||
the status field. Sometimes, the command may capture some data too e.g.
|
||||
the foreign exchange rate, or the identity of one party, or the StateRef
|
||||
of the specific input that originates the command in a bulk operation.
|
||||
This data will be used to further aid the ``Contract.verify``, because
|
||||
to ensure consistent, secure and reproducible behaviour in a distributed
|
||||
environment the ``Contract.verify``, transaction is the only allowed to
|
||||
use the content of the transaction to decide validity.
|
||||
|
||||
Another essential requirement for commands is that the correct set of
|
||||
``CompositeKeys`` are added to the Command on the builder, which will be
|
||||
used to form the set of required signers on the final validated
|
||||
transaction. These must correctly align with the expectations of the
|
||||
``Contract.verify`` method, which should be written to defensively check
|
||||
this. In particular, it is expected that at minimum the owner of an
|
||||
asset would have to be signing to permission transfer of that asset. In
|
||||
addition, other signatories will often be required e.g. an Oracle
|
||||
identity for an Oracle command, or both parties when there is an
|
||||
exchange of assets.
|
||||
|
||||
Generating Outputs
|
||||
------------------
|
||||
|
||||
Having located a set of ``StateAndRefs`` as the transaction inputs, the
|
||||
flow has to generate the output states. Typically, this is a simple call
|
||||
to the Kotlin ``copy`` method to modify the few fields that will
|
||||
transitioned in the transaction. The contract code may provide a
|
||||
``generateXXX`` method to help with this process if the task is more
|
||||
complicated. With a workflow state a slightly modified copy state is
|
||||
usually sufficient, especially as it is expected that we wish to preserve
|
||||
the ``linearId`` between state revisions, so that Vault queries can find
|
||||
the latest revision.
|
||||
|
||||
For fungible contract states such as ``Cash`` it is common to distribute
|
||||
and split the total amount e.g. to produce a remaining balance output
|
||||
state for the original owner when breaking up a large amount input
|
||||
state. Remember that the result of a successful transaction is always to
|
||||
fully consume/spend the input states, so this is required to conserve
|
||||
the total cash. For example from the demo code:
|
||||
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 2
|
||||
:end-before: DOCEND 2
|
||||
|
||||
Building the WireTransaction
|
||||
----------------------------
|
||||
|
||||
Having gathered all the ingredients for the transaction we now need to
|
||||
use a ``TransactionBuilder`` to construct the full ``WireTransaction``.
|
||||
The initial ``TransactionBuilder`` should be created by calling the
|
||||
``TransactionType.General.Builder`` method. (The other
|
||||
``TransactionBuilder`` implementation is only used for the ``NotaryChange`` flow where
|
||||
``ContractStates`` need moving to a different Notary.) At this point the
|
||||
Notary to associate with the states should be recorded. Then we keep
|
||||
adding inputs, outputs, commands and attachments to fill the
|
||||
transaction. Examples of this process are:
|
||||
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 2
|
||||
:end-before: DOCEND 2
|
||||
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 3
|
||||
:end-before: DOCEND 3
|
||||
|
||||
Completing the SignedTransaction
|
||||
--------------------------------
|
||||
|
||||
Having created an initial ``WireTransaction`` and converted this to an
|
||||
initial ``SignedTransaction`` the process of verifying and forming a
|
||||
full ``SignedTransaction`` begins and then completes with the
|
||||
notarisation. In practice this is a relatively stereotypical process,
|
||||
because assuming the ``WireTransaction`` is correctly constructed the
|
||||
verification should be immediate. However, it is also important to
|
||||
recheck the business details of any data received back from an external
|
||||
node, because a malicious party could always modify the contents before
|
||||
returning the transaction. Each remote flow should therefore check as
|
||||
much as possible of the initial ``SignedTransaction`` inside the ``unwrap`` of
|
||||
the receive before agreeing to sign. Any issues should immediately throw
|
||||
an exception to abort the flow. Similarly the originator, should always
|
||||
apply any new signatures to its original proposal to ensure the contents
|
||||
of the transaction has not been altered by the remote parties.
|
||||
|
||||
The typical code therefore checks the received ``SignedTransaction``
|
||||
using the ``verifySignatures`` method, but excluding itself, the notary
|
||||
and any other parties yet to apply their signature. The contents of the
|
||||
``WireTransaction`` inside the ``SignedTransaction`` should be fully
|
||||
verified further by expanding with ``toLedgerTransaction`` and calling
|
||||
``verify``. Further context specific and business checks should then be
|
||||
made, because the ``Contract.verify`` is not allowed to access external
|
||||
context. For example the flow may need to check that the parties are the
|
||||
right ones, or that the ``Command`` present on the transaction is as
|
||||
expected for this specific flow. An example of this from the demo code is:
|
||||
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 3
|
||||
:end-before: DOCEND 3
|
||||
|
||||
After verification the remote flow will return its signature to the
|
||||
originator. The originator should apply that signature to the starting
|
||||
``SignedTransaction`` and recheck the signatures match.
|
||||
|
||||
Committing the Transaction
|
||||
--------------------------
|
||||
|
||||
Once all the party signatures are applied to the SignedTransaction the
|
||||
final step is notarisation. This involves calling ``NotaryFlow.Client``
|
||||
to confirm the transaction, consume the inputs and return its confirming
|
||||
signature. Then the flow should ensure that all nodes end with all
|
||||
signatures and that they call ``ServiceHub.recordTransactions``. The
|
||||
code for this is standardised in the ``FinalityFlow``, or more explictly
|
||||
an example is:
|
||||
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt
|
||||
:language: kotlin
|
||||
:start-after: DOCSTART 4
|
||||
:end-before: DOCEND 4
|
||||
|
||||
Partially Visible Transactions
|
||||
------------------------------
|
||||
|
||||
The discussion so far has assumed that the parties need full visibility
|
||||
of the transaction to sign. However, there may be situations where each
|
||||
party needs to store private data for audit purposes, or for evidence to
|
||||
a regulator, but does not wish to share that with the other trading
|
||||
partner. The tear-off/Merkle tree support in Corda allows flows to send
|
||||
portions of the full transaction to restrict visibility to remote
|
||||
parties. To do this one can use the
|
||||
``WireTransaction.buildFilteredTransaction`` extension method to produce
|
||||
a ``FilteredTransaction``. The elements of the ``SignedTransaction``
|
||||
which we wish to be hide will be replaced with their secure hash. The
|
||||
overall transaction txid is still provable from the
|
||||
``FilteredTransaction`` preventing change of the private data, but we do
|
||||
not expose that data to the other node directly. A full example of this
|
||||
can be found in the ``NodeInterestRates`` Oracle code from the
|
||||
``irs-demo`` project which interacts with the ``RatesFixFlow`` flow.
|
||||
Also, refer to the :doc:`merkle-trees` documentation.
|
Loading…
x
Reference in New Issue
Block a user