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:
Matthew Nesbit
2016-11-28 13:39:34 +00:00
parent 4012d4b136
commit 4a504ca3dc
7 changed files with 1094 additions and 0 deletions

View File

@ -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))
}
}

View File

@ -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.
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}