mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
CORDA-2005: FinalityFlow has been made into an inlined flow to resolve issue with FinalityHandler (#4050)
FinalityHandler is insecure in that it is open to receive any transaction from any party. Any CorDapp targeting platform version 4 or above is required use the new c'tors which take in FlowSession objects to the counterpart flow. This flow must subcall ReceiveFinalityFlow to receive and record the finalised transaction. Old CorDapps (with target platform version < 4) will continue to work as previously. However if there are no old CorDapps loaded then the node will disable FinalityHandler.
This commit is contained in:
parent
8e6d4b4b38
commit
e8b6f5f2f2
@ -2304,7 +2304,7 @@ public final class net.corda.core.flows.ReceiveStateAndRefFlow extends net.corda
|
|||||||
@NotNull
|
@NotNull
|
||||||
public java.util.List<net.corda.core.contracts.StateAndRef<T>> call()
|
public java.util.List<net.corda.core.contracts.StateAndRef<T>> call()
|
||||||
##
|
##
|
||||||
public final class net.corda.core.flows.ReceiveTransactionFlow extends net.corda.core.flows.FlowLogic
|
public class net.corda.core.flows.ReceiveTransactionFlow extends net.corda.core.flows.FlowLogic
|
||||||
public <init>(net.corda.core.flows.FlowSession)
|
public <init>(net.corda.core.flows.FlowSession)
|
||||||
public <init>(net.corda.core.flows.FlowSession, boolean)
|
public <init>(net.corda.core.flows.FlowSession, boolean)
|
||||||
public <init>(net.corda.core.flows.FlowSession, boolean, net.corda.core.node.StatesToRecord)
|
public <init>(net.corda.core.flows.FlowSession, boolean, net.corda.core.node.StatesToRecord)
|
||||||
|
@ -28,6 +28,9 @@ import java.util.*
|
|||||||
*/
|
*/
|
||||||
@StartableByRPC
|
@StartableByRPC
|
||||||
@InitiatingFlow
|
@InitiatingFlow
|
||||||
|
// TODO Make this non-initiating as otherwise any CorDapp using confidential identities will cause its node to have an
|
||||||
|
// open door where any counterparty will be able to swap identities at will. Instead SwapIdentitiesFlow and its counterpart,
|
||||||
|
// SwapIdentitiesHandler, should be in-lined and called by CorDapp specfic-flows.
|
||||||
class SwapIdentitiesFlow(private val otherParty: Party,
|
class SwapIdentitiesFlow(private val otherParty: Party,
|
||||||
private val revocationEnabled: Boolean,
|
private val revocationEnabled: Boolean,
|
||||||
override val progressTracker: ProgressTracker) : FlowLogic<LinkedHashMap<Party, AnonymousParty>>() {
|
override val progressTracker: ProgressTracker) : FlowLogic<LinkedHashMap<Party, AnonymousParty>>() {
|
||||||
|
@ -18,8 +18,8 @@ import net.corda.testing.core.ALICE_NAME
|
|||||||
import net.corda.testing.core.BOB_NAME
|
import net.corda.testing.core.BOB_NAME
|
||||||
import net.corda.testing.core.CHARLIE_NAME
|
import net.corda.testing.core.CHARLIE_NAME
|
||||||
import net.corda.testing.core.singleIdentity
|
import net.corda.testing.core.singleIdentity
|
||||||
|
import net.corda.testing.node.internal.FINANCE_CORDAPP
|
||||||
import net.corda.testing.node.internal.InternalMockNetwork
|
import net.corda.testing.node.internal.InternalMockNetwork
|
||||||
import net.corda.testing.node.internal.cordappsForPackages
|
|
||||||
import net.corda.testing.node.internal.startFlow
|
import net.corda.testing.node.internal.startFlow
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
@ -35,7 +35,7 @@ class IdentitySyncFlowTests {
|
|||||||
fun before() {
|
fun before() {
|
||||||
// We run this in parallel threads to help catch any race conditions that may exist.
|
// We run this in parallel threads to help catch any race conditions that may exist.
|
||||||
mockNet = InternalMockNetwork(
|
mockNet = InternalMockNetwork(
|
||||||
cordappsForAllNodes = cordappsForPackages("net.corda.finance.contracts.asset", "net.corda.finance.schemas"),
|
cordappsForAllNodes = listOf(FINANCE_CORDAPP),
|
||||||
networkSendManuallyPumped = false,
|
networkSendManuallyPumped = false,
|
||||||
threadPerNode = true
|
threadPerNode = true
|
||||||
)
|
)
|
||||||
|
@ -187,9 +187,9 @@ class CollectSignatureFlow(val partiallySignedTx: SignedTransaction, val session
|
|||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* // Invoke the subFlow, in response to the counterparty calling [CollectSignaturesFlow].
|
* // Invoke the subFlow, in response to the counterparty calling [CollectSignaturesFlow].
|
||||||
* val stx = subFlow(flow)
|
* val expectedTxId = subFlow(flow).id
|
||||||
*
|
*
|
||||||
* return waitForLedgerCommit(stx.id)
|
* return subFlow(ReceiveFinalityFlow(otherPartySession, expectedTxId))
|
||||||
* }
|
* }
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
package net.corda.core.flows
|
package net.corda.core.flows
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.crypto.isFulfilledBy
|
import net.corda.core.crypto.isFulfilledBy
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.identity.groupAbstractPartyByWellKnownParty
|
import net.corda.core.identity.groupAbstractPartyByWellKnownParty
|
||||||
|
import net.corda.core.internal.cordapp.CordappInfoResolver
|
||||||
import net.corda.core.internal.pushToLoggingContext
|
import net.corda.core.internal.pushToLoggingContext
|
||||||
|
import net.corda.core.node.StatesToRecord
|
||||||
|
import net.corda.core.node.StatesToRecord.ONLY_RELEVANT
|
||||||
import net.corda.core.transactions.LedgerTransaction
|
import net.corda.core.transactions.LedgerTransaction
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.utilities.ProgressTracker
|
import net.corda.core.utilities.ProgressTracker
|
||||||
@ -17,23 +21,53 @@ import net.corda.core.utilities.ProgressTracker
|
|||||||
* The transaction is expected to have already been resolved: if its dependencies are not available in local
|
* The transaction is expected to have already been resolved: if its dependencies are not available in local
|
||||||
* storage, verification will fail. It must have signatures from all necessary parties other than the notary.
|
* storage, verification will fail. It must have signatures from all necessary parties other than the notary.
|
||||||
*
|
*
|
||||||
* If specified, the extra recipients are sent the given transaction. The base set of parties to inform are calculated
|
* A list of [FlowSession]s is required for each non-local participant of the transaction. These participants will receive
|
||||||
* from the contract-given set of participants.
|
* the final notarised transaction by calling [ReceiveFinalityFlow] in their counterpart flows. Sessions with non-participants
|
||||||
|
* can also be included, but they must specifiy [StatesToRecord.ALL_VISIBLE] for statesToRecortd if they wish to record the
|
||||||
|
* contract states into their vaults.
|
||||||
*
|
*
|
||||||
* The flow returns the same transaction but with the additional signatures from the notary.
|
* The flow returns the same transaction but with the additional signatures from the notary.
|
||||||
*
|
*
|
||||||
|
* NOTE: This is an inlined flow but for backwards compatibility is annotated with [InitiatingFlow].
|
||||||
|
*
|
||||||
* @param transaction What to commit.
|
* @param transaction What to commit.
|
||||||
* @param extraRecipients A list of additional participants to inform of the transaction.
|
* @param sessions A collection of [FlowSession]s who will be given the notarised transaction. This list **must** include
|
||||||
|
* all participants in the transaction (excluding the local identity).
|
||||||
*/
|
*/
|
||||||
|
// To maintain backwards compatibility with the old API, FinalityFlow can act both as an intiating flow and as an inlined flow.
|
||||||
|
// This is only possible because a flow is only truly initiating when the first call to initiateFlow is made (where the
|
||||||
|
// presence of @InitiatingFlow is checked). So the new API is inlined simply because that code path doesn't call initiateFlow.
|
||||||
@InitiatingFlow
|
@InitiatingFlow
|
||||||
class FinalityFlow(val transaction: SignedTransaction,
|
class FinalityFlow private constructor(val transaction: SignedTransaction,
|
||||||
private val extraRecipients: Set<Party>,
|
private val extraRecipients: Set<Party>,
|
||||||
override val progressTracker: ProgressTracker) : FlowLogic<SignedTransaction>() {
|
override val progressTracker: ProgressTracker,
|
||||||
constructor(transaction: SignedTransaction, extraParticipants: Set<Party>) : this(transaction, extraParticipants, tracker())
|
private val sessions: Collection<FlowSession>?) : FlowLogic<SignedTransaction>() {
|
||||||
constructor(transaction: SignedTransaction) : this(transaction, emptySet(), tracker())
|
@Deprecated(DEPRECATION_MSG)
|
||||||
constructor(transaction: SignedTransaction, progressTracker: ProgressTracker) : this(transaction, emptySet(), progressTracker)
|
constructor(transaction: SignedTransaction, extraRecipients: Set<Party>, progressTracker: ProgressTracker) : this(
|
||||||
|
transaction, extraRecipients, progressTracker, null
|
||||||
|
)
|
||||||
|
@Deprecated(DEPRECATION_MSG)
|
||||||
|
constructor(transaction: SignedTransaction, extraRecipients: Set<Party>) : this(transaction, extraRecipients, tracker(), null)
|
||||||
|
@Deprecated(DEPRECATION_MSG)
|
||||||
|
constructor(transaction: SignedTransaction) : this(transaction, emptySet(), tracker(), null)
|
||||||
|
@Deprecated(DEPRECATION_MSG)
|
||||||
|
constructor(transaction: SignedTransaction, progressTracker: ProgressTracker) : this(transaction, emptySet(), progressTracker, null)
|
||||||
|
|
||||||
|
constructor(transaction: SignedTransaction, sessions: Collection<FlowSession>, progressTracker: ProgressTracker) : this(
|
||||||
|
transaction, emptySet(), progressTracker, sessions
|
||||||
|
)
|
||||||
|
constructor(transaction: SignedTransaction, sessions: Collection<FlowSession>) : this(
|
||||||
|
transaction, emptySet(), tracker(), sessions
|
||||||
|
)
|
||||||
|
constructor(transaction: SignedTransaction, firstSession: FlowSession, vararg restSessions: FlowSession) : this(
|
||||||
|
transaction, emptySet(), tracker(), listOf(firstSession) + restSessions.asList()
|
||||||
|
)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private const val DEPRECATION_MSG = "It is unsafe to use this constructor as it requires nodes to automatically " +
|
||||||
|
"accept notarised transactions without first checking their relevancy. Instead, use one of the constructors " +
|
||||||
|
"that takes in existing FlowSessions."
|
||||||
|
|
||||||
object NOTARISING : ProgressTracker.Step("Requesting signature by notary service") {
|
object NOTARISING : ProgressTracker.Step("Requesting signature by notary service") {
|
||||||
override fun childProgressTracker() = NotaryFlow.Client.tracker()
|
override fun childProgressTracker() = NotaryFlow.Client.tracker()
|
||||||
}
|
}
|
||||||
@ -47,6 +81,19 @@ class FinalityFlow(val transaction: SignedTransaction,
|
|||||||
@Suspendable
|
@Suspendable
|
||||||
@Throws(NotaryException::class)
|
@Throws(NotaryException::class)
|
||||||
override fun call(): SignedTransaction {
|
override fun call(): SignedTransaction {
|
||||||
|
if (sessions == null) {
|
||||||
|
require(CordappInfoResolver.currentTargetVersion < 4) {
|
||||||
|
"A flow session for each external participant to the transaction must be provided. If you wish to continue " +
|
||||||
|
"using this insecure API then specify a target platform version of less than 4 for your CorDapp."
|
||||||
|
}
|
||||||
|
logger.warn("The current usage of FinalityFlow is unsafe. Please consider upgrading your CorDapp to use " +
|
||||||
|
"FinalityFlow with FlowSessions.")
|
||||||
|
} else {
|
||||||
|
require(sessions.none { serviceHub.myInfo.isLegalIdentity(it.counterparty) }) {
|
||||||
|
"Do not provide flow sessions for the local node. FinalityFlow will record the notarised transaction locally."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Note: this method is carefully broken up to minimize the amount of data reachable from the stack at
|
// Note: this method is carefully broken up to minimize the amount of data reachable from the stack at
|
||||||
// the point where subFlow is invoked, as that minimizes the checkpointing work to be done.
|
// the point where subFlow is invoked, as that minimizes the checkpointing work to be done.
|
||||||
//
|
//
|
||||||
@ -54,26 +101,49 @@ class FinalityFlow(val transaction: SignedTransaction,
|
|||||||
// Then send to the notary if needed, record locally and distribute.
|
// Then send to the notary if needed, record locally and distribute.
|
||||||
|
|
||||||
transaction.pushToLoggingContext()
|
transaction.pushToLoggingContext()
|
||||||
val commandDataTypes = transaction.tx.commands.map { it.value }.mapNotNull { it::class.qualifiedName }.distinct()
|
logCommandData()
|
||||||
logger.info("Started finalization, commands are ${commandDataTypes.joinToString(", ", "[", "]")}.")
|
val externalParticipants = extractExternalParticipants(verifyTx())
|
||||||
val parties = getPartiesToSend(verifyTx())
|
|
||||||
|
if (sessions != null) {
|
||||||
|
val missingRecipients = externalParticipants - sessions.map { it.counterparty }
|
||||||
|
require(missingRecipients.isEmpty()) {
|
||||||
|
"Flow sessions were not provided for the following transaction participants: $missingRecipients"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val notarised = notariseAndRecord()
|
val notarised = notariseAndRecord()
|
||||||
|
|
||||||
// Each transaction has its own set of recipients, but extra recipients get them all.
|
// Each transaction has its own set of recipients, but extra recipients get them all.
|
||||||
progressTracker.currentStep = BROADCASTING
|
progressTracker.currentStep = BROADCASTING
|
||||||
val recipients = parties.filterNot(serviceHub.myInfo::isLegalIdentity)
|
|
||||||
logger.info("Broadcasting transaction to parties ${recipients.map { it.name }.joinToString(", ", "[", "]")}.")
|
if (sessions == null) {
|
||||||
for (party in recipients) {
|
val recipients = externalParticipants + (extraRecipients - serviceHub.myInfo.legalIdentities)
|
||||||
logger.info("Sending transaction to party ${party.name}.")
|
logger.info("Broadcasting transaction to parties ${recipients.joinToString(", ", "[", "]")}.")
|
||||||
val session = initiateFlow(party)
|
for (recipient in recipients) {
|
||||||
subFlow(SendTransactionFlow(session, notarised))
|
logger.info("Sending transaction to party ${recipient.name}.")
|
||||||
logger.info("Party ${party.name} received the transaction.")
|
val session = initiateFlow(recipient)
|
||||||
|
subFlow(SendTransactionFlow(session, notarised))
|
||||||
|
logger.info("Party $recipient received the transaction.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (session in sessions) {
|
||||||
|
subFlow(SendTransactionFlow(session, notarised))
|
||||||
|
logger.info("Party ${session.counterparty} received the transaction.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("All parties received the transaction successfully.")
|
logger.info("All parties received the transaction successfully.")
|
||||||
|
|
||||||
return notarised
|
return notarised
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun logCommandData() {
|
||||||
|
if (logger.isDebugEnabled) {
|
||||||
|
val commandDataTypes = transaction.tx.commands.asSequence().mapNotNull { it.value::class.qualifiedName }.distinct()
|
||||||
|
logger.debug("Started finalization, commands are ${commandDataTypes.joinToString(", ", "[", "]")}.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
private fun notariseAndRecord(): SignedTransaction {
|
private fun notariseAndRecord(): SignedTransaction {
|
||||||
val notarised = if (needsNotarySignature(transaction)) {
|
val notarised = if (needsNotarySignature(transaction)) {
|
||||||
@ -98,13 +168,13 @@ class FinalityFlow(val transaction: SignedTransaction,
|
|||||||
|
|
||||||
private fun hasNoNotarySignature(stx: SignedTransaction): Boolean {
|
private fun hasNoNotarySignature(stx: SignedTransaction): Boolean {
|
||||||
val notaryKey = stx.tx.notary?.owningKey
|
val notaryKey = stx.tx.notary?.owningKey
|
||||||
val signers = stx.sigs.map { it.by }.toSet()
|
val signers = stx.sigs.asSequence().map { it.by }.toSet()
|
||||||
return notaryKey?.isFulfilledBy(signers) != true
|
return notaryKey?.isFulfilledBy(signers) != true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPartiesToSend(ltx: LedgerTransaction): Set<Party> {
|
private fun extractExternalParticipants(ltx: LedgerTransaction): Set<Party> {
|
||||||
val participants = ltx.outputStates.flatMap { it.participants } + ltx.inputStates.flatMap { it.participants }
|
val participants = ltx.outputStates.flatMap { it.participants } + ltx.inputStates.flatMap { it.participants }
|
||||||
return groupAbstractPartyByWellKnownParty(serviceHub, participants).keys + extraRecipients
|
return groupAbstractPartyByWellKnownParty(serviceHub, participants).keys - serviceHub.myInfo.legalIdentities
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun verifyTx(): LedgerTransaction {
|
private fun verifyTx(): LedgerTransaction {
|
||||||
@ -116,3 +186,33 @@ class FinalityFlow(val transaction: SignedTransaction,
|
|||||||
return ltx
|
return ltx
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The receiving counterpart to [FinalityFlow].
|
||||||
|
*
|
||||||
|
* All parties who are receiving a finalised transaction from a sender flow must subcall this flow in their own flows.
|
||||||
|
*
|
||||||
|
* It's typical to have already signed the transaction proposal in the same workflow using [SignTransactionFlow]. If so
|
||||||
|
* then the transaction ID can be passed in as an extra check to ensure the finalised transaction is the one that was signed
|
||||||
|
* before it's committed to the vault.
|
||||||
|
*
|
||||||
|
* @param otherSideSession The session which is providing the transaction to record.
|
||||||
|
* @param expectedTxId Expected ID of the transaction that's about to be received. This is typically retrieved from
|
||||||
|
* [SignTransactionFlow].
|
||||||
|
* @param statesToRecord Which transactions to commit to the vault. Defaults to [StatesToRecord.ONLY_RELEVANT].
|
||||||
|
*/
|
||||||
|
class ReceiveFinalityFlow @JvmOverloads constructor(val otherSideSession: FlowSession,
|
||||||
|
val expectedTxId: SecureHash? = null,
|
||||||
|
val statesToRecord: StatesToRecord = ONLY_RELEVANT) : FlowLogic<SignedTransaction>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call(): SignedTransaction {
|
||||||
|
return subFlow(object : ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = true, statesToRecord = statesToRecord) {
|
||||||
|
override fun checkBeforeRecording(stx: SignedTransaction) {
|
||||||
|
require(expectedTxId == null || expectedTxId == stx.id) {
|
||||||
|
"We expected to receive transaction with ID $expectedTxId but instead got ${stx.id}. Transaction was" +
|
||||||
|
"not recorded and nor its states sent to the vault."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -23,9 +23,9 @@ import java.security.SignatureException
|
|||||||
* @property checkSufficientSignatures if true checks all required signatures are present. See [SignedTransaction.verify].
|
* @property checkSufficientSignatures if true checks all required signatures are present. See [SignedTransaction.verify].
|
||||||
* @property statesToRecord which transaction states should be recorded in the vault, if any.
|
* @property statesToRecord which transaction states should be recorded in the vault, if any.
|
||||||
*/
|
*/
|
||||||
class ReceiveTransactionFlow @JvmOverloads constructor(private val otherSideSession: FlowSession,
|
open class ReceiveTransactionFlow @JvmOverloads constructor(private val otherSideSession: FlowSession,
|
||||||
private val checkSufficientSignatures: Boolean = true,
|
private val checkSufficientSignatures: Boolean = true,
|
||||||
private val statesToRecord: StatesToRecord = StatesToRecord.NONE) : FlowLogic<SignedTransaction>() {
|
private val statesToRecord: StatesToRecord = StatesToRecord.NONE) : FlowLogic<SignedTransaction>() {
|
||||||
@Suppress("KDocMissingDocumentation")
|
@Suppress("KDocMissingDocumentation")
|
||||||
@Suspendable
|
@Suspendable
|
||||||
@Throws(SignatureException::class,
|
@Throws(SignatureException::class,
|
||||||
@ -40,7 +40,7 @@ class ReceiveTransactionFlow @JvmOverloads constructor(private val otherSideSess
|
|||||||
}
|
}
|
||||||
val stx = otherSideSession.receive<SignedTransaction>().unwrap {
|
val stx = otherSideSession.receive<SignedTransaction>().unwrap {
|
||||||
it.pushToLoggingContext()
|
it.pushToLoggingContext()
|
||||||
logger.info("Received transaction acknowledgement request from party ${otherSideSession.counterparty.name}.")
|
logger.info("Received transaction acknowledgement request from party ${otherSideSession.counterparty}.")
|
||||||
subFlow(ResolveTransactionsFlow(it, otherSideSession))
|
subFlow(ResolveTransactionsFlow(it, otherSideSession))
|
||||||
logger.info("Transaction dependencies resolution completed.")
|
logger.info("Transaction dependencies resolution completed.")
|
||||||
try {
|
try {
|
||||||
@ -54,12 +54,21 @@ class ReceiveTransactionFlow @JvmOverloads constructor(private val otherSideSess
|
|||||||
if (checkSufficientSignatures) {
|
if (checkSufficientSignatures) {
|
||||||
// We should only send a transaction to the vault for processing if we did in fact fully verify it, and
|
// We should only send a transaction to the vault for processing if we did in fact fully verify it, and
|
||||||
// there are no missing signatures. We don't want partly signed stuff in the vault.
|
// there are no missing signatures. We don't want partly signed stuff in the vault.
|
||||||
|
checkBeforeRecording(stx)
|
||||||
logger.info("Successfully received fully signed tx. Sending it to the vault for processing.")
|
logger.info("Successfully received fully signed tx. Sending it to the vault for processing.")
|
||||||
serviceHub.recordTransactions(statesToRecord, setOf(stx))
|
serviceHub.recordTransactions(statesToRecord, setOf(stx))
|
||||||
logger.info("Successfully recorded received transaction locally.")
|
logger.info("Successfully recorded received transaction locally.")
|
||||||
}
|
}
|
||||||
return stx
|
return stx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to perform extra checks on the received transaction just before it's recorded. The transaction has already
|
||||||
|
* been resolved and verified at this point.
|
||||||
|
*/
|
||||||
|
@Suspendable
|
||||||
|
@Throws(FlowException::class)
|
||||||
|
protected open fun checkBeforeRecording(stx: SignedTransaction) = Unit
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -74,7 +83,8 @@ class ReceiveStateAndRefFlow<out T : ContractState>(private val otherSideSession
|
|||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call(): List<StateAndRef<T>> {
|
override fun call(): List<StateAndRef<T>> {
|
||||||
return otherSideSession.receive<List<StateAndRef<T>>>().unwrap {
|
return otherSideSession.receive<List<StateAndRef<T>>>().unwrap {
|
||||||
subFlow(ResolveTransactionsFlow(it.map { it.ref.txhash }.toSet(), otherSideSession))
|
val txHashes = it.asSequence().map { it.ref.txhash }.toSet()
|
||||||
|
subFlow(ResolveTransactionsFlow(txHashes, otherSideSession))
|
||||||
it
|
it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package net.corda.core.internal.cordapp
|
package net.corda.core.internal.cordapp
|
||||||
|
|
||||||
|
import net.corda.core.internal.PLATFORM_VERSION
|
||||||
import net.corda.core.internal.VisibleForTesting
|
import net.corda.core.internal.VisibleForTesting
|
||||||
import net.corda.core.utilities.loggerFor
|
import net.corda.core.utilities.loggerFor
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
@ -11,12 +12,12 @@ object CordappInfoResolver {
|
|||||||
private val logger = loggerFor<CordappInfoResolver>()
|
private val logger = loggerFor<CordappInfoResolver>()
|
||||||
private val cordappClasses: ConcurrentHashMap<String, Set<CordappImpl.Info>> = ConcurrentHashMap()
|
private val cordappClasses: ConcurrentHashMap<String, Set<CordappImpl.Info>> = ConcurrentHashMap()
|
||||||
|
|
||||||
// TODO use the StackWalker API once we migrate to Java 9+
|
// TODO Use the StackWalker API once we migrate to Java 9+
|
||||||
private var cordappInfoResolver: () -> CordappImpl.Info? = {
|
private var cordappInfoResolver: () -> CordappImpl.Info? = {
|
||||||
Exception().stackTrace
|
Exception().stackTrace
|
||||||
.mapNotNull { cordappClasses[it.className] }
|
.mapNotNull { cordappClasses[it.className] }
|
||||||
// If there is more than one cordapp registered for a class name we can't determine the "correct" one and return null.
|
// If there is more than one cordapp registered for a class name we can't determine the "correct" one and return null.
|
||||||
.firstOrNull { it.size < 2 }?.single()
|
.firstOrNull { it.size == 1 }?.single()
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -42,22 +43,33 @@ object CordappInfoResolver {
|
|||||||
* In situations where a `[CordappProvider]` is available the CorDapp context should be obtained from there.
|
* In situations where a `[CordappProvider]` is available the CorDapp context should be obtained from there.
|
||||||
*
|
*
|
||||||
* @return Information about the CorDapp from which the invoker is called, null if called outside a CorDapp or the
|
* @return Information about the CorDapp from which the invoker is called, null if called outside a CorDapp or the
|
||||||
* calling CorDapp cannot be reliably determined..
|
* calling CorDapp cannot be reliably determined.
|
||||||
*/
|
*/
|
||||||
fun getCorDappInfo(): CordappImpl.Info? = cordappInfoResolver()
|
val currentCordappInfo: CordappImpl.Info? get() = cordappInfoResolver()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Temporarily switch out the internal resolver for another one. For use in testing.
|
* Returns the target version of the current calling CorDapp. Defaults to the current platform version if there isn't one.
|
||||||
|
*/
|
||||||
|
// TODO It may be the default is wrong and this should be Int? instead
|
||||||
|
val currentTargetVersion: Int get() = currentCordappInfo?.targetPlatformVersion ?: PLATFORM_VERSION
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Temporarily apply a fake CorDapp.Info with the given parameters. For use in testing.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
@Synchronized
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
fun withCordappInfoResolution(tempResolver: () -> CordappImpl.Info?, block: () -> Unit) {
|
fun <T> withCordappInfo(shortName: String = "CordappInfoResolver.withCordappInfo",
|
||||||
val resolver = cordappInfoResolver
|
vendor: String = "Corda",
|
||||||
cordappInfoResolver = tempResolver
|
version: String = "1.0",
|
||||||
|
minimumPlatformVersion: Int = 1,
|
||||||
|
targetPlatformVersion: Int = PLATFORM_VERSION,
|
||||||
|
block: () -> T): T {
|
||||||
|
val currentResolver = cordappInfoResolver
|
||||||
|
cordappInfoResolver = { CordappImpl.Info(shortName, vendor, version, minimumPlatformVersion, targetPlatformVersion) }
|
||||||
try {
|
try {
|
||||||
block()
|
return block()
|
||||||
} finally {
|
} finally {
|
||||||
cordappInfoResolver = resolver
|
cordappInfoResolver = currentResolver
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,4 +77,4 @@ object CordappInfoResolver {
|
|||||||
internal fun clear() {
|
internal fun clear() {
|
||||||
cordappClasses.clear()
|
cordappClasses.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,6 @@ import com.natpryce.hamkrest.assertion.assert
|
|||||||
import net.corda.core.contracts.Command
|
import net.corda.core.contracts.Command
|
||||||
import net.corda.core.contracts.StateAndContract
|
import net.corda.core.contracts.StateAndContract
|
||||||
import net.corda.core.contracts.requireThat
|
import net.corda.core.contracts.requireThat
|
||||||
import net.corda.testing.internal.matchers.flow.willReturn
|
|
||||||
import net.corda.testing.internal.matchers.flow.willThrow
|
|
||||||
import net.corda.core.flows.mixins.WithContracts
|
import net.corda.core.flows.mixins.WithContracts
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
@ -17,6 +15,8 @@ import net.corda.core.transactions.SignedTransaction
|
|||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
import net.corda.testing.contracts.DummyContract
|
import net.corda.testing.contracts.DummyContract
|
||||||
import net.corda.testing.core.*
|
import net.corda.testing.core.*
|
||||||
|
import net.corda.testing.internal.matchers.flow.willReturn
|
||||||
|
import net.corda.testing.internal.matchers.flow.willThrow
|
||||||
import net.corda.testing.internal.rigorousMock
|
import net.corda.testing.internal.rigorousMock
|
||||||
import net.corda.testing.node.MockServices
|
import net.corda.testing.node.MockServices
|
||||||
import net.corda.testing.node.internal.InternalMockNetwork
|
import net.corda.testing.node.internal.InternalMockNetwork
|
||||||
@ -116,7 +116,7 @@ class CollectSignaturesFlowTests : WithContracts {
|
|||||||
val ptx = serviceHub.signInitialTransaction(builder)
|
val ptx = serviceHub.signInitialTransaction(builder)
|
||||||
val sessions = excludeHostNode(serviceHub, groupAbstractPartyByWellKnownParty(serviceHub, state.owners)).map { initiateFlow(it.key) }
|
val sessions = excludeHostNode(serviceHub, groupAbstractPartyByWellKnownParty(serviceHub, state.owners)).map { initiateFlow(it.key) }
|
||||||
val stx = subFlow(CollectSignaturesFlow(ptx, sessions, myInputKeys))
|
val stx = subFlow(CollectSignaturesFlow(ptx, sessions, myInputKeys))
|
||||||
return subFlow(FinalityFlow(stx))
|
return subFlow(FinalityFlow(stx, sessions))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,8 +136,8 @@ class CollectSignaturesFlowTests : WithContracts {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val stx = subFlow(signFlow)
|
val stxId = subFlow(signFlow).id
|
||||||
waitForLedgerCommit(stx.id)
|
subFlow(ReceiveFinalityFlow(otherSideSession, expectedTxId = stxId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,63 +4,98 @@ import com.natpryce.hamkrest.and
|
|||||||
import com.natpryce.hamkrest.assertion.assert
|
import com.natpryce.hamkrest.assertion.assert
|
||||||
import net.corda.core.flows.mixins.WithFinality
|
import net.corda.core.flows.mixins.WithFinality
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.internal.cordapp.CordappInfoResolver
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
|
import net.corda.core.utilities.getOrThrow
|
||||||
import net.corda.finance.POUNDS
|
import net.corda.finance.POUNDS
|
||||||
import net.corda.finance.contracts.asset.Cash
|
import net.corda.finance.contracts.asset.Cash
|
||||||
import net.corda.finance.issuedBy
|
import net.corda.finance.issuedBy
|
||||||
import net.corda.testing.core.*
|
import net.corda.testing.core.*
|
||||||
import net.corda.testing.internal.matchers.flow.willReturn
|
import net.corda.testing.internal.matchers.flow.willReturn
|
||||||
import net.corda.testing.internal.matchers.flow.willThrow
|
import net.corda.testing.internal.matchers.flow.willThrow
|
||||||
import net.corda.testing.node.internal.InternalMockNetwork
|
import net.corda.testing.node.TestCordapp
|
||||||
import net.corda.testing.node.internal.TestStartedNode
|
import net.corda.testing.node.internal.*
|
||||||
import net.corda.testing.node.internal.cordappsForPackages
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.AfterClass
|
import org.assertj.core.api.Assertions.assertThatIllegalArgumentException
|
||||||
|
import org.junit.After
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class FinalityFlowTests : WithFinality {
|
class FinalityFlowTests : WithFinality {
|
||||||
companion object {
|
companion object {
|
||||||
private val CHARLIE = TestIdentity(CHARLIE_NAME, 90).party
|
private val CHARLIE = TestIdentity(CHARLIE_NAME, 90).party
|
||||||
private val classMockNet = InternalMockNetwork(cordappsForAllNodes = cordappsForPackages(
|
|
||||||
"net.corda.finance.contracts.asset",
|
|
||||||
"net.corda.finance.schemas"
|
|
||||||
))
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
@AfterClass
|
|
||||||
fun tearDown() = classMockNet.stopNodes()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override val mockNet = classMockNet
|
override val mockNet = InternalMockNetwork(cordappsForAllNodes = cordappsForPackages(
|
||||||
|
"net.corda.finance.contracts.asset",
|
||||||
|
"net.corda.finance.schemas",
|
||||||
|
"net.corda.core.flows.mixins"
|
||||||
|
))
|
||||||
|
|
||||||
private val aliceNode = makeNode(ALICE_NAME)
|
private val aliceNode = makeNode(ALICE_NAME)
|
||||||
private val bobNode = makeNode(BOB_NAME)
|
|
||||||
|
|
||||||
private val bob = bobNode.info.singleIdentity()
|
|
||||||
private val notary = mockNet.defaultNotaryIdentity
|
private val notary = mockNet.defaultNotaryIdentity
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() = mockNet.stopNodes()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `finalise a simple transaction`() {
|
fun `finalise a simple transaction`() {
|
||||||
val stx = aliceNode.signCashTransactionWith(bob)
|
val bob = createBob()
|
||||||
|
val stx = aliceNode.issuesCashTo(bob)
|
||||||
|
|
||||||
assert.that(
|
assert.that(
|
||||||
aliceNode.finalise(stx),
|
aliceNode.finalise(stx, bob.info.singleIdentity()),
|
||||||
willReturn(
|
willReturn(
|
||||||
requiredSignatures(1)
|
requiredSignatures(1)
|
||||||
and visibleTo(bobNode)))
|
and visibleTo(bob)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `reject a transaction with unknown parties`() {
|
fun `reject a transaction with unknown parties`() {
|
||||||
// Charlie isn't part of this network, so node A won't recognise them
|
// Charlie isn't part of this network, so node A won't recognise them
|
||||||
val stx = aliceNode.signCashTransactionWith(CHARLIE)
|
val stx = aliceNode.issuesCashTo(CHARLIE)
|
||||||
|
|
||||||
assert.that(
|
assert.that(
|
||||||
aliceNode.finalise(stx),
|
aliceNode.finalise(stx),
|
||||||
willThrow<IllegalArgumentException>())
|
willThrow<IllegalArgumentException>())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun TestStartedNode.signCashTransactionWith(other: Party): SignedTransaction {
|
@Test
|
||||||
|
fun `prevent use of the old API if the CorDapp target version is 4`() {
|
||||||
|
val bob = createBob()
|
||||||
|
val stx = aliceNode.issuesCashTo(bob)
|
||||||
|
val resultFuture = CordappInfoResolver.withCordappInfo(targetPlatformVersion = 4) {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
aliceNode.startFlowAndRunNetwork(FinalityFlow(stx)).resultFuture
|
||||||
|
}
|
||||||
|
assertThatIllegalArgumentException().isThrownBy {
|
||||||
|
resultFuture.getOrThrow()
|
||||||
|
}.withMessageContaining("A flow session for each external participant to the transaction must be provided.")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `allow use of the old API if the CorDapp target version is 3`() {
|
||||||
|
// We need Bob to load at least one old CorDapp so that its FinalityHandler is enabled
|
||||||
|
val bob = createBob(cordapps = listOf(cordappForPackages("com.template").withTargetVersion(3)))
|
||||||
|
val stx = aliceNode.issuesCashTo(bob)
|
||||||
|
val resultFuture = CordappInfoResolver.withCordappInfo(targetPlatformVersion = 3) {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
aliceNode.startFlowAndRunNetwork(FinalityFlow(stx)).resultFuture
|
||||||
|
}
|
||||||
|
resultFuture.getOrThrow()
|
||||||
|
assertThat(bob.services.validatedTransactions.getTransaction(stx.id)).isNotNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createBob(cordapps: List<TestCordapp> = emptyList()): TestStartedNode {
|
||||||
|
return mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME, additionalCordapps = cordapps))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TestStartedNode.issuesCashTo(recipient: TestStartedNode): SignedTransaction {
|
||||||
|
return issuesCashTo(recipient.info.singleIdentity())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TestStartedNode.issuesCashTo(other: Party): SignedTransaction {
|
||||||
val amount = 1000.POUNDS.issuedBy(info.singleIdentity().ref(0))
|
val amount = 1000.POUNDS.issuedBy(info.singleIdentity().ref(0))
|
||||||
val builder = TransactionBuilder(notary)
|
val builder = TransactionBuilder(notary)
|
||||||
Cash().generateIssue(builder, amount, other, notary)
|
Cash().generateIssue(builder, amount, other, notary)
|
||||||
|
@ -50,7 +50,7 @@ internal class CreateRefState : FlowLogic<SignedTransaction>() {
|
|||||||
addOutputState(RefState.State(ourIdentity), RefState.CONTRACT_ID)
|
addOutputState(RefState.State(ourIdentity), RefState.CONTRACT_ID)
|
||||||
addCommand(RefState.Create(), listOf(ourIdentity.owningKey))
|
addCommand(RefState.Create(), listOf(ourIdentity.owningKey))
|
||||||
})
|
})
|
||||||
return subFlow(FinalityFlow(stx))
|
return subFlow(FinalityFlow(stx, emptyList()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,7 +64,7 @@ internal class UpdateRefState(private val stateAndRef: StateAndRef<RefState.Stat
|
|||||||
addOutputState(stateAndRef.state.data.update(), RefState.CONTRACT_ID)
|
addOutputState(stateAndRef.state.data.update(), RefState.CONTRACT_ID)
|
||||||
addCommand(RefState.Update(), listOf(ourIdentity.owningKey))
|
addCommand(RefState.Update(), listOf(ourIdentity.owningKey))
|
||||||
})
|
})
|
||||||
return subFlow(FinalityFlow(stx))
|
return subFlow(FinalityFlow(stx, emptyList()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,7 +111,7 @@ internal class UseRefState(private val linearId: UniqueIdentifier) : FlowLogic<S
|
|||||||
addOutputState(DummyState(), DummyContract.PROGRAM_ID)
|
addOutputState(DummyState(), DummyContract.PROGRAM_ID)
|
||||||
addCommand(DummyContract.Commands.Create(), listOf(ourIdentity.owningKey))
|
addCommand(DummyContract.Commands.Create(), listOf(ourIdentity.owningKey))
|
||||||
})
|
})
|
||||||
return subFlow(FinalityFlow(stx))
|
return subFlow(FinalityFlow(stx, emptyList()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,9 +4,7 @@ import co.paralleluniverse.fibers.Suspendable
|
|||||||
import com.natpryce.hamkrest.MatchResult
|
import com.natpryce.hamkrest.MatchResult
|
||||||
import com.natpryce.hamkrest.Matcher
|
import com.natpryce.hamkrest.Matcher
|
||||||
import com.natpryce.hamkrest.equalTo
|
import com.natpryce.hamkrest.equalTo
|
||||||
import net.corda.core.flows.FinalityFlow
|
import net.corda.core.flows.*
|
||||||
import net.corda.core.flows.FlowLogic
|
|
||||||
import net.corda.core.flows.StartableByRPC
|
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.internal.FlowStateMachine
|
import net.corda.core.internal.FlowStateMachine
|
||||||
import net.corda.core.messaging.CordaRPCOps
|
import net.corda.core.messaging.CordaRPCOps
|
||||||
@ -18,16 +16,16 @@ import net.corda.testing.node.internal.TestStartedNode
|
|||||||
|
|
||||||
interface WithFinality : WithMockNet {
|
interface WithFinality : WithMockNet {
|
||||||
//region Operations
|
//region Operations
|
||||||
fun TestStartedNode.finalise(stx: SignedTransaction, vararg additionalParties: Party): FlowStateMachine<SignedTransaction> {
|
fun TestStartedNode.finalise(stx: SignedTransaction, vararg recipients: Party): FlowStateMachine<SignedTransaction> {
|
||||||
return startFlowAndRunNetwork(FinalityFlow(stx, additionalParties.toSet()))
|
return startFlowAndRunNetwork(FinalityInvoker(stx, recipients.toSet()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun TestStartedNode.getValidatedTransaction(stx: SignedTransaction): SignedTransaction {
|
fun TestStartedNode.getValidatedTransaction(stx: SignedTransaction): SignedTransaction {
|
||||||
return services.validatedTransactions.getTransaction(stx.id)!!
|
return services.validatedTransactions.getTransaction(stx.id)!!
|
||||||
}
|
}
|
||||||
|
|
||||||
fun CordaRPCOps.finalise(stx: SignedTransaction, vararg parties: Party): FlowHandle<SignedTransaction> {
|
fun CordaRPCOps.finalise(stx: SignedTransaction, vararg recipients: Party): FlowHandle<SignedTransaction> {
|
||||||
return startFlow(::FinalityInvoker, stx, parties.toSet()).andRunNetwork()
|
return startFlow(::FinalityInvoker, stx, recipients.toSet()).andRunNetwork()
|
||||||
}
|
}
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
@ -40,10 +38,22 @@ interface WithFinality : WithMockNet {
|
|||||||
}
|
}
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
|
@InitiatingFlow
|
||||||
@StartableByRPC
|
@StartableByRPC
|
||||||
class FinalityInvoker(private val transaction: SignedTransaction,
|
class FinalityInvoker(private val transaction: SignedTransaction,
|
||||||
private val extraRecipients: Set<Party>) : FlowLogic<SignedTransaction>() {
|
private val recipients: Set<Party>) : FlowLogic<SignedTransaction>() {
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call(): SignedTransaction = subFlow(FinalityFlow(transaction, extraRecipients))
|
override fun call(): SignedTransaction {
|
||||||
|
val sessions = recipients.map(::initiateFlow)
|
||||||
|
return subFlow(FinalityFlow(transaction, sessions))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@InitiatedBy(FinalityInvoker::class)
|
||||||
|
class FinalityResponder(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call() {
|
||||||
|
subFlow(ReceiveFinalityFlow(otherSide))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,42 +1,37 @@
|
|||||||
package net.corda.core.internal.cordapp
|
package net.corda.core.internal.cordapp
|
||||||
|
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
class CordappInfoResolverTest {
|
class CordappInfoResolverTest {
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
@After
|
@After
|
||||||
fun clearCordappInfoResolver() {
|
fun clearCordappInfoResolver() {
|
||||||
CordappInfoResolver.clear()
|
CordappInfoResolver.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test()
|
@Test
|
||||||
fun `The correct cordapp resolver is used after calling withCordappResolution`() {
|
fun `the correct cordapp resolver is used after calling withCordappInfo`() {
|
||||||
val defaultTargetVersion = 222
|
val defaultTargetVersion = 222
|
||||||
|
|
||||||
CordappInfoResolver.register(listOf(javaClass.name), CordappImpl.Info("test", "test", "2", 3, defaultTargetVersion))
|
CordappInfoResolver.register(listOf(javaClass.name), CordappImpl.Info("test", "test", "2", 3, defaultTargetVersion))
|
||||||
assertEquals(defaultTargetVersion, returnCallingTargetVersion())
|
assertEquals(defaultTargetVersion, CordappInfoResolver.currentTargetVersion)
|
||||||
|
|
||||||
val expectedTargetVersion = 555
|
val expectedTargetVersion = 555
|
||||||
CordappInfoResolver.withCordappInfoResolution( { CordappImpl.Info("foo", "bar", "1", 2, expectedTargetVersion) })
|
CordappInfoResolver.withCordappInfo(targetPlatformVersion = expectedTargetVersion) {
|
||||||
{
|
val actualTargetVersion = CordappInfoResolver.currentTargetVersion
|
||||||
val actualTargetVersion = returnCallingTargetVersion()
|
|
||||||
assertEquals(expectedTargetVersion, actualTargetVersion)
|
assertEquals(expectedTargetVersion, actualTargetVersion)
|
||||||
}
|
}
|
||||||
assertEquals(defaultTargetVersion, returnCallingTargetVersion())
|
assertEquals(defaultTargetVersion, CordappInfoResolver.currentTargetVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test()
|
@Test
|
||||||
fun `When more than one cordapp is registered for the same class, the resolver returns null`() {
|
fun `when more than one cordapp is registered for the same class, the resolver returns null`() {
|
||||||
CordappInfoResolver.register(listOf(javaClass.name), CordappImpl.Info("test", "test", "2", 3, 222))
|
CordappInfoResolver.register(listOf(javaClass.name), CordappImpl.Info("test", "test", "2", 3, 222))
|
||||||
CordappInfoResolver.register(listOf(javaClass.name), CordappImpl.Info("test1", "test1", "1", 2, 456))
|
CordappInfoResolver.register(listOf(javaClass.name), CordappImpl.Info("test1", "test1", "1", 2, 456))
|
||||||
assertEquals(0, returnCallingTargetVersion())
|
assertThat(CordappInfoResolver.currentCordappInfo).isNull()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private fun returnCallingTargetVersion(): Int {
|
|
||||||
return CordappInfoResolver.getCorDappInfo()?.targetPlatformVersion ?: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -522,6 +522,9 @@ An example is the ``@InitiatingFlow InitiatorFlow``/``@InitiatedBy ResponderFlow
|
|||||||
|
|
||||||
.. note:: Initiating flows are versioned separately from their parents.
|
.. note:: Initiating flows are versioned separately from their parents.
|
||||||
|
|
||||||
|
.. note:: The only exception to this rule is ``FinalityFlow`` which is annotated with ``@InitiatingFlow`` but is an inlined flow. This flow
|
||||||
|
was previously initiating and the annotation exists to maintain backwards compatibility with old code.
|
||||||
|
|
||||||
Core initiating subflows
|
Core initiating subflows
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
Corda-provided initiating subflows are a little different to standard ones as they are versioned together with the
|
Corda-provided initiating subflows are a little different to standard ones as they are versioned together with the
|
||||||
@ -532,8 +535,6 @@ Library flows
|
|||||||
^^^^^^^^^^^^^
|
^^^^^^^^^^^^^
|
||||||
Corda installs four initiating subflow pairs on each node by default:
|
Corda installs four initiating subflow pairs on each node by default:
|
||||||
|
|
||||||
* ``FinalityFlow``/``FinalityHandler``, which should be used to notarise and record a transaction and broadcast it to
|
|
||||||
all relevant parties
|
|
||||||
* ``NotaryChangeFlow``/``NotaryChangeHandler``, which should be used to change a state's notary
|
* ``NotaryChangeFlow``/``NotaryChangeHandler``, which should be used to change a state's notary
|
||||||
* ``ContractUpgradeFlow.Initiate``/``ContractUpgradeHandler``, which should be used to change a state's contract
|
* ``ContractUpgradeFlow.Initiate``/``ContractUpgradeHandler``, which should be used to change a state's contract
|
||||||
* ``SwapIdentitiesFlow``/``SwapIdentitiesHandler``, which is used to exchange confidential identities with a
|
* ``SwapIdentitiesFlow``/``SwapIdentitiesHandler``, which is used to exchange confidential identities with a
|
||||||
@ -546,10 +547,13 @@ Corda installs four initiating subflow pairs on each node by default:
|
|||||||
Corda also provides a number of built-in inlined subflows that should be used for handling common tasks. The most
|
Corda also provides a number of built-in inlined subflows that should be used for handling common tasks. The most
|
||||||
important are:
|
important are:
|
||||||
|
|
||||||
* ``CollectSignaturesFlow`` (inlined), which should be used to collect a transaction's required signatures
|
* ``FinalityFlow`` which is used to notarise, record locally and then broadcast a signed transaction to its participants
|
||||||
* ``SendTransactionFlow`` (inlined), which should be used to send a signed transaction if it needed to be resolved on
|
and any extra parties.
|
||||||
|
* ``ReceiveFinalityFlow`` to receive these notarised transactions from the ``FinalityFlow`` sender and record locally.
|
||||||
|
* ``CollectSignaturesFlow`` , which should be used to collect a transaction's required signatures
|
||||||
|
* ``SendTransactionFlow`` , which should be used to send a signed transaction if it needed to be resolved on
|
||||||
the other side.
|
the other side.
|
||||||
* ``ReceiveTransactionFlow`` (inlined), which should be used receive a signed transaction
|
* ``ReceiveTransactionFlow``, which should be used receive a signed transaction
|
||||||
|
|
||||||
Let's look at some of these flows in more detail.
|
Let's look at some of these flows in more detail.
|
||||||
|
|
||||||
@ -588,20 +592,26 @@ We can also choose to send the transaction to additional parties who aren't one
|
|||||||
:end-before: DOCEND 10
|
:end-before: DOCEND 10
|
||||||
:dedent: 12
|
:dedent: 12
|
||||||
|
|
||||||
Only one party has to call ``FinalityFlow`` for a given transaction to be recorded by all participants. It does
|
Only one party has to call ``FinalityFlow`` for a given transaction to be recorded by all participants. It **must not**
|
||||||
**not** need to be called by each participant individually.
|
be called by every participant. Instead, every other particpant **must** call ``ReceiveFinalityFlow`` in their responder
|
||||||
|
flow to receive the transaction:
|
||||||
|
|
||||||
Because the transaction has already been notarised and the input states consumed, if the participants when receiving the
|
.. container:: codeset
|
||||||
transaction fail to verify it, or the receiving flow (the finality handler) fails due to some other error, we then have
|
|
||||||
the scenario where not all parties have the correct up to date view of the ledger. To recover from this the finality handler
|
|
||||||
is automatically sent to the flow hospital where it's suspended and retried from its last checkpoint on node restart.
|
|
||||||
This gives the node operator the opportunity to recover from the error. Until the issue is resolved the node will continue
|
|
||||||
to retry the flow on each startup.
|
|
||||||
|
|
||||||
.. note:: It's possible to forcibly terminate the erroring finality handler using the ``killFlow`` RPC but at the risk
|
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FlowCookbook.kt
|
||||||
of an inconsistent view of the ledger.
|
:language: kotlin
|
||||||
|
:start-after: DOCSTART ReceiveFinalityFlow
|
||||||
|
:end-before: DOCEND ReceiveFinalityFlow
|
||||||
|
:dedent: 8
|
||||||
|
|
||||||
.. note:: A future release will allow retrying hospitalised flows without restarting the node, i.e. via RPC.
|
.. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/java/FlowCookbook.java
|
||||||
|
:language: java
|
||||||
|
:start-after: DOCSTART ReceiveFinalityFlow
|
||||||
|
:end-before: DOCEND ReceiveFinalityFlow
|
||||||
|
:dedent: 12
|
||||||
|
|
||||||
|
``idOfTxWeSigned`` is an optional parameter used to confirm that we got the right transaction. It comes from using ``SignTransactionFlow``
|
||||||
|
which is described below.
|
||||||
|
|
||||||
CollectSignaturesFlow/SignTransactionFlow
|
CollectSignaturesFlow/SignTransactionFlow
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
@ -137,23 +137,23 @@ Use the ``ServiceHub`` ``jdbcSession`` function to obtain a JDBC connection as i
|
|||||||
:start-after: DOCSTART JdbcSession
|
:start-after: DOCSTART JdbcSession
|
||||||
:end-before: DOCEND JdbcSession
|
:end-before: DOCEND JdbcSession
|
||||||
|
|
||||||
JDBC session's can be used in Flows and Service Plugins (see ":doc:`flow-state-machines`")
|
JDBC sessions can be used in flows and services (see ":doc:`flow-state-machines`").
|
||||||
|
|
||||||
The following example illustrates the creation of a custom corda service using a jdbcSession:
|
The following example illustrates the creation of a custom Corda service using a ``jdbcSession``:
|
||||||
|
|
||||||
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/CustomVaultQuery.kt
|
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/vault/CustomVaultQuery.kt
|
||||||
:language: kotlin
|
:language: kotlin
|
||||||
:start-after: DOCSTART CustomVaultQuery
|
:start-after: DOCSTART CustomVaultQuery
|
||||||
:end-before: DOCEND CustomVaultQuery
|
:end-before: DOCEND CustomVaultQuery
|
||||||
|
|
||||||
which is then referenced within a custom flow:
|
which is then referenced within a custom flow:
|
||||||
|
|
||||||
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/CustomVaultQuery.kt
|
.. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/vault/CustomVaultQuery.kt
|
||||||
:language: kotlin
|
:language: kotlin
|
||||||
:start-after: DOCSTART TopupIssuer
|
:start-after: DOCSTART TopupIssuer
|
||||||
:end-before: DOCEND TopupIssuer
|
:end-before: DOCEND TopupIssuer
|
||||||
|
|
||||||
For examples on testing ``@CordaService`` implementations, see the oracle example :doc:`here <oracles>`
|
For examples on testing ``@CordaService`` implementations, see the oracle example :doc:`here <oracles>`.
|
||||||
|
|
||||||
JPA Support
|
JPA Support
|
||||||
-----------
|
-----------
|
||||||
|
@ -24,7 +24,7 @@ are considered to have a stake in the state. Among other things, the ``participa
|
|||||||
|
|
||||||
* Need to sign any notary-change and contract-upgrade transactions involving this state
|
* Need to sign any notary-change and contract-upgrade transactions involving this state
|
||||||
|
|
||||||
* Receive any finalised transactions involving this state as part of ``FinalityFlow``
|
* Receive any finalised transactions involving this state as part of ``FinalityFlow`` / ``ReceiveFinalityFlow``
|
||||||
|
|
||||||
ContractState sub-interfaces
|
ContractState sub-interfaces
|
||||||
----------------------------
|
----------------------------
|
||||||
|
@ -12,9 +12,21 @@ Unreleased
|
|||||||
|
|
||||||
* New "validate-configuration" sub-command to `corda.jar`, allowing to validate the actual node configuration without starting the node.
|
* New "validate-configuration" sub-command to `corda.jar`, allowing to validate the actual node configuration without starting the node.
|
||||||
|
|
||||||
* Introduced new optional network bootstrapper command line option (--minimum-platform-version) to set as a network parameter
|
* CorDapps now have the ability to specify a minimum platform version in their MANIFEST.MF to prevent old nodes from loading them.
|
||||||
|
|
||||||
* Introduce minimum and target platform version for CorDapps.
|
* CorDapps have the ability to specify a target platform version in their MANIFEST.MF as a means of indicating to the node
|
||||||
|
the app was designed and tested on that version.
|
||||||
|
|
||||||
|
* Nodes will no longer automatically reject flow initiation requests for flows they don't know about. Instead the request will remain
|
||||||
|
un-acknowledged in the message broker. This enables the recovery scenerio whereby any missing CorDapp can be installed and retried on node
|
||||||
|
restart. As a consequence the initiating flow will be blocked until the receiving node has resolved the issue.
|
||||||
|
|
||||||
|
* ``FinalityFlow`` is now an inlined flow and no longer requires a handler flow in the counterparty. This is to fix the
|
||||||
|
security problem with the handler flow as it accepts any transaction it receives without any checks. Existing CorDapp
|
||||||
|
binaries relying on this old behaviour will continue to function as previously. However, it is strongly recommended that
|
||||||
|
CorDapps switch to this new API. See :doc:`upgrade-notes` for further details.
|
||||||
|
|
||||||
|
* Introduced new optional network bootstrapper command line option (--minimum-platform-version) to set as a network parameter
|
||||||
|
|
||||||
* BFT-Smart and Raft notary implementations have been extracted out of node into ``experimental`` CorDapps to emphasise
|
* BFT-Smart and Raft notary implementations have been extracted out of node into ``experimental`` CorDapps to emphasise
|
||||||
their experimental nature. Moreover, the BFT-Smart notary will only work in dev mode due to its use of Java serialization.
|
their experimental nature. Moreover, the BFT-Smart notary will only work in dev mode due to its use of Java serialization.
|
||||||
|
@ -85,12 +85,12 @@ flow with a full node.
|
|||||||
|
|
||||||
.. container:: codeset
|
.. container:: codeset
|
||||||
|
|
||||||
.. literalinclude:: ../../docs/source/example-code/src/integration-test/kotlin/net/corda/docs/TutorialFlowAsyncOperationTest.kt
|
.. literalinclude:: ../../docs/source/example-code/src/integration-test/kotlin/net/corda/docs/kotlin/tutorial/test/TutorialFlowAsyncOperationTest.kt
|
||||||
:language: kotlin
|
:language: kotlin
|
||||||
:start-after: DOCSTART summingWorks
|
:start-after: DOCSTART summingWorks
|
||||||
:end-before: DOCEND summingWorks
|
:end-before: DOCEND summingWorks
|
||||||
|
|
||||||
.. literalinclude:: ../../docs/source/example-code/src/integration-test/java/net/corda/docs/java/TutorialFlowAsyncOperationTest.java
|
.. literalinclude:: ../../docs/source/example-code/src/integration-test/java/net/corda/docs/java/tutorial/test/TutorialFlowAsyncOperationTest.java
|
||||||
:language: java
|
:language: java
|
||||||
:start-after: DOCSTART summingWorks
|
:start-after: DOCSTART summingWorks
|
||||||
:end-before: DOCEND summingWorks
|
:end-before: DOCEND summingWorks
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package net.corda.docs;
|
package net.corda.docs.java.tutorial.test;
|
||||||
|
|
||||||
import net.corda.client.rpc.CordaRPCClient;
|
import net.corda.client.rpc.CordaRPCClient;
|
||||||
import net.corda.core.concurrent.CordaFuture;
|
import net.corda.core.concurrent.CordaFuture;
|
||||||
@ -18,7 +18,9 @@ import net.corda.testing.node.User;
|
|||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import rx.Observable;
|
import rx.Observable;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.Currency;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import static java.util.Arrays.asList;
|
import static java.util.Arrays.asList;
|
||||||
import static java.util.Collections.singletonList;
|
import static java.util.Collections.singletonList;
|
||||||
@ -38,7 +40,7 @@ public class JavaIntegrationTestingTutorial {
|
|||||||
// START 1
|
// START 1
|
||||||
driver(new DriverParameters()
|
driver(new DriverParameters()
|
||||||
.withStartNodesInProcess(true)
|
.withStartNodesInProcess(true)
|
||||||
.withExtraCordappPackagesToScan(Arrays.asList("net.corda.finance.contracts.asset", "net.corda.finance.schemas")), dsl -> {
|
.withExtraCordappPackagesToScan(singletonList("net.corda.finance")), dsl -> {
|
||||||
|
|
||||||
User aliceUser = new User("aliceUser", "testPassword1", new HashSet<>(asList(
|
User aliceUser = new User("aliceUser", "testPassword1", new HashSet<>(asList(
|
||||||
startFlow(CashIssueAndPaymentFlow.class),
|
startFlow(CashIssueAndPaymentFlow.class),
|
@ -1,4 +1,4 @@
|
|||||||
package net.corda.docs.java;
|
package net.corda.docs.java.tutorial.test;
|
||||||
|
|
||||||
import kotlin.Unit;
|
import kotlin.Unit;
|
||||||
import net.corda.client.rpc.CordaRPCClient;
|
import net.corda.client.rpc.CordaRPCClient;
|
@ -1,4 +1,4 @@
|
|||||||
package net.corda.docs
|
package net.corda.docs.kotlin.tutorial.test
|
||||||
|
|
||||||
import net.corda.client.rpc.CordaRPCClient
|
import net.corda.client.rpc.CordaRPCClient
|
||||||
import net.corda.core.contracts.Amount
|
import net.corda.core.contracts.Amount
|
||||||
@ -29,10 +29,7 @@ class KotlinIntegrationTestingTutorial {
|
|||||||
@Test
|
@Test
|
||||||
fun `alice bob cash exchange example`() {
|
fun `alice bob cash exchange example`() {
|
||||||
// START 1
|
// START 1
|
||||||
driver(DriverParameters(
|
driver(DriverParameters(startNodesInProcess = true, extraCordappPackagesToScan = listOf("net.corda.finance"))) {
|
||||||
startNodesInProcess = true,
|
|
||||||
extraCordappPackagesToScan = listOf("net.corda.finance.contracts.asset", "net.corda.finance.schemas")
|
|
||||||
)) {
|
|
||||||
val aliceUser = User("aliceUser", "testPassword1", permissions = setOf(
|
val aliceUser = User("aliceUser", "testPassword1", permissions = setOf(
|
||||||
startFlow<CashIssueAndPaymentFlow>(),
|
startFlow<CashIssueAndPaymentFlow>(),
|
||||||
invokeRpc("vaultTrackBy")
|
invokeRpc("vaultTrackBy")
|
@ -1,4 +1,4 @@
|
|||||||
package net.corda.docs
|
package net.corda.docs.kotlin.tutorial.test
|
||||||
|
|
||||||
import net.corda.client.rpc.CordaRPCClient
|
import net.corda.client.rpc.CordaRPCClient
|
||||||
import net.corda.core.messaging.startFlow
|
import net.corda.core.messaging.startFlow
|
@ -0,0 +1,134 @@
|
|||||||
|
package net.corda.docs.java;
|
||||||
|
|
||||||
|
import co.paralleluniverse.fibers.Suspendable;
|
||||||
|
import net.corda.core.flows.*;
|
||||||
|
import net.corda.core.identity.Party;
|
||||||
|
import net.corda.core.transactions.SignedTransaction;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import static java.util.Collections.singletonList;
|
||||||
|
|
||||||
|
@SuppressWarnings("ALL")
|
||||||
|
public class FinalityFlowMigration {
|
||||||
|
public static SignedTransaction dummyTransactionWithParticipant(Party party) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// DOCSTART SimpleFlowUsingOldApi
|
||||||
|
public static class SimpleFlowUsingOldApi extends FlowLogic<SignedTransaction> {
|
||||||
|
private final Party counterparty;
|
||||||
|
|
||||||
|
@Suspendable
|
||||||
|
@Override
|
||||||
|
public SignedTransaction call() throws FlowException {
|
||||||
|
SignedTransaction stx = dummyTransactionWithParticipant(counterparty);
|
||||||
|
return subFlow(new FinalityFlow(stx));
|
||||||
|
}
|
||||||
|
// DOCEND SimpleFlowUsingOldApi
|
||||||
|
|
||||||
|
public SimpleFlowUsingOldApi(Party counterparty) {
|
||||||
|
this.counterparty = counterparty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DOCSTART SimpleFlowUsingNewApi
|
||||||
|
// Notice how the flow *must* now be an initiating flow even when it wasn't before.
|
||||||
|
@InitiatingFlow
|
||||||
|
public static class SimpleFlowUsingNewApi extends FlowLogic<SignedTransaction> {
|
||||||
|
private final Party counterparty;
|
||||||
|
|
||||||
|
@Suspendable
|
||||||
|
@Override
|
||||||
|
public SignedTransaction call() throws FlowException {
|
||||||
|
SignedTransaction stx = dummyTransactionWithParticipant(counterparty);
|
||||||
|
// For each non-local participant in the transaction we must initiate a flow session with them.
|
||||||
|
FlowSession session = initiateFlow(counterparty);
|
||||||
|
return subFlow(new FinalityFlow(stx, session));
|
||||||
|
}
|
||||||
|
// DOCEND SimpleFlowUsingNewApi
|
||||||
|
|
||||||
|
public SimpleFlowUsingNewApi(Party counterparty) {
|
||||||
|
this.counterparty = counterparty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// DOCSTART SimpleNewResponderFlow
|
||||||
|
// All participants will run this flow to receive and record the finalised transaction into their vault.
|
||||||
|
@InitiatedBy(SimpleFlowUsingNewApi.class)
|
||||||
|
public static class SimpleNewResponderFlow extends FlowLogic<Void> {
|
||||||
|
private final FlowSession otherSide;
|
||||||
|
|
||||||
|
@Suspendable
|
||||||
|
@Override
|
||||||
|
public Void call() throws FlowException {
|
||||||
|
subFlow(new ReceiveFinalityFlow(otherSide));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// DOCEND SimpleNewResponderFlow
|
||||||
|
|
||||||
|
public SimpleNewResponderFlow(FlowSession otherSide) {
|
||||||
|
this.otherSide = otherSide;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DOCSTART ExistingInitiatingFlow
|
||||||
|
// Assuming the previous version of the flow was 1 (the default if none is specified), we increment the version number to 2
|
||||||
|
// to allow for backwards compatibility with nodes running the old CorDapp.
|
||||||
|
@InitiatingFlow(version = 2)
|
||||||
|
public static class ExistingInitiatingFlow extends FlowLogic<SignedTransaction> {
|
||||||
|
private final Party counterparty;
|
||||||
|
|
||||||
|
@Suspendable
|
||||||
|
@Override
|
||||||
|
public SignedTransaction call() throws FlowException {
|
||||||
|
SignedTransaction partiallySignedTx = dummyTransactionWithParticipant(counterparty);
|
||||||
|
FlowSession session = initiateFlow(counterparty);
|
||||||
|
SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow(partiallySignedTx, singletonList(session)));
|
||||||
|
// Determine which version of the flow that other side is using.
|
||||||
|
if (session.getCounterpartyFlowInfo().getFlowVersion() == 1) {
|
||||||
|
// Use the old API if the other side is using the previous version of the flow.
|
||||||
|
return subFlow(new FinalityFlow(fullySignedTx));
|
||||||
|
} else {
|
||||||
|
// Otherwise they're at least on version 2 and so we can send the finalised transaction on the existing session.
|
||||||
|
return subFlow(new FinalityFlow(fullySignedTx, session));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// DOCEND ExistingInitiatingFlow
|
||||||
|
|
||||||
|
public ExistingInitiatingFlow(Party counterparty) {
|
||||||
|
this.counterparty = counterparty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@InitiatedBy(ExistingInitiatingFlow.class)
|
||||||
|
public static class ExistingResponderFlow extends FlowLogic<Void> {
|
||||||
|
private final FlowSession otherSide;
|
||||||
|
|
||||||
|
public ExistingResponderFlow(FlowSession otherSide) {
|
||||||
|
this.otherSide = otherSide;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suspendable
|
||||||
|
@Override
|
||||||
|
public Void call() throws FlowException {
|
||||||
|
SignedTransaction txWeJustSigned = subFlow(new SignTransactionFlow(otherSide) {
|
||||||
|
@Suspendable
|
||||||
|
@Override
|
||||||
|
protected void checkTransaction(@NotNull SignedTransaction stx) throws FlowException {
|
||||||
|
// Do checks here
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// DOCSTART ExistingResponderFlow
|
||||||
|
if (otherSide.getCounterpartyFlowInfo().getFlowVersion() >= 2) {
|
||||||
|
// The other side is not using the old CorDapp so call ReceiveFinalityFlow to record the finalised transaction.
|
||||||
|
// If SignTransactionFlow is used then we can verify the tranaction we receive for recording is the same one
|
||||||
|
// that was just signed.
|
||||||
|
subFlow(new ReceiveFinalityFlow(otherSide, txWeJustSigned.getId()));
|
||||||
|
} else {
|
||||||
|
// Otherwise the other side is running the old CorDapp and so we don't need to do anything further. The node
|
||||||
|
// will automatically record the finalised transaction using the old insecure mechanism.
|
||||||
|
}
|
||||||
|
// DOCEND ExistingResponderFlow
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -28,8 +28,8 @@ import java.security.GeneralSecurityException;
|
|||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import static com.google.common.base.Preconditions.checkArgument;
|
import static com.google.common.base.Preconditions.checkArgument;
|
||||||
import static java.util.Collections.*;
|
import static java.util.Collections.*;
|
||||||
@ -578,13 +578,13 @@ public class FlowCookbook {
|
|||||||
// We notarise the transaction and get it recorded in the vault of
|
// We notarise the transaction and get it recorded in the vault of
|
||||||
// the participants of all the transaction's states.
|
// the participants of all the transaction's states.
|
||||||
// DOCSTART 09
|
// DOCSTART 09
|
||||||
SignedTransaction notarisedTx1 = subFlow(new FinalityFlow(fullySignedTx, FINALISATION.childProgressTracker()));
|
SignedTransaction notarisedTx1 = subFlow(new FinalityFlow(fullySignedTx, singleton(counterpartySession), FINALISATION.childProgressTracker()));
|
||||||
// DOCEND 09
|
// DOCEND 09
|
||||||
// We can also choose to send it to additional parties who aren't one
|
// We can also choose to send it to additional parties who aren't one
|
||||||
// of the state's participants.
|
// of the state's participants.
|
||||||
// DOCSTART 10
|
// DOCSTART 10
|
||||||
Set<Party> additionalParties = singleton(regulator);
|
List<FlowSession> partySessions = Arrays.asList(counterpartySession, initiateFlow(regulator));
|
||||||
SignedTransaction notarisedTx2 = subFlow(new FinalityFlow(fullySignedTx, additionalParties, FINALISATION.childProgressTracker()));
|
SignedTransaction notarisedTx2 = subFlow(new FinalityFlow(fullySignedTx, partySessions, FINALISATION.childProgressTracker()));
|
||||||
// DOCEND 10
|
// DOCEND 10
|
||||||
|
|
||||||
// DOCSTART FlowSession porting
|
// DOCSTART FlowSession porting
|
||||||
@ -673,7 +673,7 @@ public class FlowCookbook {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
subFlow(new SignTxFlow(counterpartySession, SignTransactionFlow.tracker()));
|
SecureHash idOfTxWeSigned = subFlow(new SignTxFlow(counterpartySession, SignTransactionFlow.tracker())).getId();
|
||||||
// DOCEND 16
|
// DOCEND 16
|
||||||
|
|
||||||
/*------------------------------
|
/*------------------------------
|
||||||
@ -681,9 +681,12 @@ public class FlowCookbook {
|
|||||||
------------------------------*/
|
------------------------------*/
|
||||||
progressTracker.setCurrentStep(FINALISATION);
|
progressTracker.setCurrentStep(FINALISATION);
|
||||||
|
|
||||||
// Nothing to do here! As long as some other party calls
|
// As the final step the responder waits to receive the notarised transaction from the sending party
|
||||||
// ``FinalityFlow``, the recording of the transaction on our node
|
// Since it knows the ID of the transaction it just signed, the transaction ID is specified to ensure the correct
|
||||||
// we be handled automatically.
|
// transaction is received and recorded.
|
||||||
|
// DOCSTART ReceiveFinalityFlow
|
||||||
|
subFlow(new ReceiveFinalityFlow(counterpartySession, idOfTxWeSigned));
|
||||||
|
// DOCEND ReceiveFinalityFlow
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -2,16 +2,12 @@ package net.corda.docs.java.tutorial.helloworld;
|
|||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable;
|
import co.paralleluniverse.fibers.Suspendable;
|
||||||
import com.template.TemplateContract;
|
import com.template.TemplateContract;
|
||||||
import net.corda.core.flows.FlowException;
|
import net.corda.core.flows.*;
|
||||||
import net.corda.core.flows.FlowLogic;
|
|
||||||
import net.corda.core.flows.InitiatingFlow;
|
|
||||||
import net.corda.core.flows.StartableByRPC;
|
|
||||||
import net.corda.core.utilities.ProgressTracker;
|
import net.corda.core.utilities.ProgressTracker;
|
||||||
|
|
||||||
// DOCSTART 01
|
// DOCSTART 01
|
||||||
// Add these imports:
|
// Add these imports:
|
||||||
import net.corda.core.contracts.Command;
|
import net.corda.core.contracts.Command;
|
||||||
import net.corda.core.flows.FinalityFlow;
|
|
||||||
import net.corda.core.identity.Party;
|
import net.corda.core.identity.Party;
|
||||||
import net.corda.core.transactions.SignedTransaction;
|
import net.corda.core.transactions.SignedTransaction;
|
||||||
import net.corda.core.transactions.TransactionBuilder;
|
import net.corda.core.transactions.TransactionBuilder;
|
||||||
@ -59,8 +55,11 @@ public class IOUFlow extends FlowLogic<Void> {
|
|||||||
// Signing the transaction.
|
// Signing the transaction.
|
||||||
SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder);
|
SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder);
|
||||||
|
|
||||||
// Finalising the transaction.
|
// Creating a session with the other party.
|
||||||
subFlow(new FinalityFlow(signedTx));
|
FlowSession otherPartySession = initiateFlow(otherParty);
|
||||||
|
|
||||||
|
// We finalise the transaction and then send it to the counterparty.
|
||||||
|
subFlow(new FinalityFlow(signedTx, otherPartySession));
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
package net.corda.docs.java.tutorial.helloworld;
|
||||||
|
|
||||||
|
import co.paralleluniverse.fibers.Suspendable;
|
||||||
|
import net.corda.core.flows.*;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
// DOCSTART 01
|
||||||
|
// Replace Responder's definition with:
|
||||||
|
@InitiatedBy(IOUFlow.class)
|
||||||
|
public class IOUFlowResponder extends FlowLogic<Void> {
|
||||||
|
private final FlowSession otherPartySession;
|
||||||
|
|
||||||
|
public IOUFlowResponder(FlowSession otherPartySession) {
|
||||||
|
this.otherPartySession = otherPartySession;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suspendable
|
||||||
|
@Override
|
||||||
|
public Void call() throws FlowException {
|
||||||
|
subFlow(new ReceiveFinalityFlow(otherPartySession));
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// DOCEND 01
|
@ -69,7 +69,7 @@ public class IOUFlow extends FlowLogic<Void> {
|
|||||||
signedTx, Arrays.asList(otherPartySession), CollectSignaturesFlow.tracker()));
|
signedTx, Arrays.asList(otherPartySession), CollectSignaturesFlow.tracker()));
|
||||||
|
|
||||||
// Finalising the transaction.
|
// Finalising the transaction.
|
||||||
subFlow(new FinalityFlow(fullySignedTx));
|
subFlow(new FinalityFlow(fullySignedTx, otherPartySession));
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
// DOCEND 02
|
// DOCEND 02
|
||||||
|
@ -1,15 +1,14 @@
|
|||||||
package net.corda.docs.java.tutorial.twoparty;
|
package net.corda.docs.java.tutorial.twoparty;
|
||||||
|
|
||||||
// DOCSTART 01
|
|
||||||
// Add these imports:
|
|
||||||
import co.paralleluniverse.fibers.Suspendable;
|
import co.paralleluniverse.fibers.Suspendable;
|
||||||
import net.corda.core.contracts.ContractState;
|
import net.corda.core.contracts.ContractState;
|
||||||
|
import net.corda.core.crypto.SecureHash;
|
||||||
import net.corda.core.flows.*;
|
import net.corda.core.flows.*;
|
||||||
import net.corda.core.transactions.SignedTransaction;
|
import net.corda.core.transactions.SignedTransaction;
|
||||||
import net.corda.core.utilities.ProgressTracker;
|
|
||||||
|
|
||||||
import static net.corda.core.contracts.ContractsDSL.requireThat;
|
import static net.corda.core.contracts.ContractsDSL.requireThat;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
// Define IOUFlowResponder:
|
// Define IOUFlowResponder:
|
||||||
@InitiatedBy(IOUFlow.class)
|
@InitiatedBy(IOUFlow.class)
|
||||||
public class IOUFlowResponder extends FlowLogic<Void> {
|
public class IOUFlowResponder extends FlowLogic<Void> {
|
||||||
@ -22,9 +21,10 @@ public class IOUFlowResponder extends FlowLogic<Void> {
|
|||||||
@Suspendable
|
@Suspendable
|
||||||
@Override
|
@Override
|
||||||
public Void call() throws FlowException {
|
public Void call() throws FlowException {
|
||||||
|
// DOCSTART 01
|
||||||
class SignTxFlow extends SignTransactionFlow {
|
class SignTxFlow extends SignTransactionFlow {
|
||||||
private SignTxFlow(FlowSession otherPartySession, ProgressTracker progressTracker) {
|
private SignTxFlow(FlowSession otherPartySession) {
|
||||||
super(otherPartySession, progressTracker);
|
super(otherPartySession);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -39,9 +39,11 @@ public class IOUFlowResponder extends FlowLogic<Void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
subFlow(new SignTxFlow(otherPartySession, SignTransactionFlow.Companion.tracker()));
|
SecureHash expectedTxId = subFlow(new SignTxFlow(otherPartySession)).getId();
|
||||||
|
|
||||||
|
subFlow(new ReceiveFinalityFlow(otherPartySession, expectedTxId));
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
// DOCEND 01
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// DOCEND 01
|
|
@ -0,0 +1,91 @@
|
|||||||
|
@file:Suppress("DEPRECATION", "unused", "UNUSED_PARAMETER")
|
||||||
|
|
||||||
|
package net.corda.docs.kotlin
|
||||||
|
|
||||||
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
|
import net.corda.core.flows.*
|
||||||
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.transactions.SignedTransaction
|
||||||
|
|
||||||
|
private fun dummyTransactionWithParticipant(party: Party): SignedTransaction = TODO()
|
||||||
|
|
||||||
|
// DOCSTART SimpleFlowUsingOldApi
|
||||||
|
class SimpleFlowUsingOldApi(private val counterparty: Party) : FlowLogic<SignedTransaction>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call(): SignedTransaction {
|
||||||
|
val stx = dummyTransactionWithParticipant(counterparty)
|
||||||
|
return subFlow(FinalityFlow(stx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// DOCEND SimpleFlowUsingOldApi
|
||||||
|
|
||||||
|
// DOCSTART SimpleFlowUsingNewApi
|
||||||
|
// Notice how the flow *must* now be an initiating flow even when it wasn't before.
|
||||||
|
@InitiatingFlow
|
||||||
|
class SimpleFlowUsingNewApi(private val counterparty: Party) : FlowLogic<SignedTransaction>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call(): SignedTransaction {
|
||||||
|
val stx = dummyTransactionWithParticipant(counterparty)
|
||||||
|
// For each non-local participant in the transaction we must initiate a flow session with them.
|
||||||
|
val session = initiateFlow(counterparty)
|
||||||
|
return subFlow(FinalityFlow(stx, session))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// DOCEND SimpleFlowUsingNewApi
|
||||||
|
|
||||||
|
// DOCSTART SimpleNewResponderFlow
|
||||||
|
// All participants will run this flow to receive and record the finalised transaction into their vault.
|
||||||
|
@InitiatedBy(SimpleFlowUsingNewApi::class)
|
||||||
|
class SimpleNewResponderFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call() {
|
||||||
|
subFlow(ReceiveFinalityFlow(otherSide))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// DOCEND SimpleNewResponderFlow
|
||||||
|
|
||||||
|
// DOCSTART ExistingInitiatingFlow
|
||||||
|
// Assuming the previous version of the flow was 1 (the default if none is specified), we increment the version number to 2
|
||||||
|
// to allow for backwards compatibility with nodes running the old CorDapp.
|
||||||
|
@InitiatingFlow(version = 2)
|
||||||
|
class ExistingInitiatingFlow(private val counterparty: Party) : FlowLogic<SignedTransaction>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call(): SignedTransaction {
|
||||||
|
val partiallySignedTx = dummyTransactionWithParticipant(counterparty)
|
||||||
|
val session = initiateFlow(counterparty)
|
||||||
|
val fullySignedTx = subFlow(CollectSignaturesFlow(partiallySignedTx, listOf(session)))
|
||||||
|
// Determine which version of the flow that other side is using.
|
||||||
|
return if (session.getCounterpartyFlowInfo().flowVersion == 1) {
|
||||||
|
// Use the old API if the other side is using the previous version of the flow.
|
||||||
|
subFlow(FinalityFlow(fullySignedTx))
|
||||||
|
} else {
|
||||||
|
// Otherwise they're at least on version 2 and so we can send the finalised transaction on the existing session.
|
||||||
|
subFlow(FinalityFlow(fullySignedTx, session))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// DOCEND ExistingInitiatingFlow
|
||||||
|
|
||||||
|
@InitiatedBy(ExistingInitiatingFlow::class)
|
||||||
|
class ExistingResponderFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call() {
|
||||||
|
val txWeJustSigned = subFlow(object : SignTransactionFlow(otherSide) {
|
||||||
|
@Suspendable
|
||||||
|
override fun checkTransaction(stx: SignedTransaction) {
|
||||||
|
// Do checks here
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// DOCSTART ExistingResponderFlow
|
||||||
|
if (otherSide.getCounterpartyFlowInfo().flowVersion >= 2) {
|
||||||
|
// The other side is not using the old CorDapp so call ReceiveFinalityFlow to record the finalised transaction.
|
||||||
|
// If SignTransactionFlow is used then we can verify the tranaction we receive for recording is the same one
|
||||||
|
// that was just signed.
|
||||||
|
subFlow(ReceiveFinalityFlow(otherSide, expectedTxId = txWeJustSigned.id))
|
||||||
|
} else {
|
||||||
|
// Otherwise the other side is running the old CorDapp and so we don't need to do anything further. The node
|
||||||
|
// will automatically record the finalised transaction using the old insecure mechanism.
|
||||||
|
}
|
||||||
|
// DOCEND ExistingResponderFlow
|
||||||
|
}
|
||||||
|
}
|
@ -568,13 +568,13 @@ class InitiatorFlow(val arg1: Boolean, val arg2: Int, private val counterparty:
|
|||||||
// We notarise the transaction and get it recorded in the vault of
|
// We notarise the transaction and get it recorded in the vault of
|
||||||
// the participants of all the transaction's states.
|
// the participants of all the transaction's states.
|
||||||
// DOCSTART 09
|
// DOCSTART 09
|
||||||
val notarisedTx1: SignedTransaction = subFlow(FinalityFlow(fullySignedTx, FINALISATION.childProgressTracker()))
|
val notarisedTx1: SignedTransaction = subFlow(FinalityFlow(fullySignedTx, listOf(counterpartySession), FINALISATION.childProgressTracker()))
|
||||||
// DOCEND 09
|
// DOCEND 09
|
||||||
// We can also choose to send it to additional parties who aren't one
|
// We can also choose to send it to additional parties who aren't one
|
||||||
// of the state's participants.
|
// of the state's participants.
|
||||||
// DOCSTART 10
|
// DOCSTART 10
|
||||||
val additionalParties: Set<Party> = setOf(regulator)
|
val partySessions: List<FlowSession> = listOf(counterpartySession, initiateFlow(regulator))
|
||||||
val notarisedTx2: SignedTransaction = subFlow(FinalityFlow(fullySignedTx, additionalParties, FINALISATION.childProgressTracker()))
|
val notarisedTx2: SignedTransaction = subFlow(FinalityFlow(fullySignedTx, partySessions, FINALISATION.childProgressTracker()))
|
||||||
// DOCEND 10
|
// DOCEND 10
|
||||||
|
|
||||||
// DOCSTART FlowSession porting
|
// DOCSTART FlowSession porting
|
||||||
@ -650,7 +650,7 @@ class ResponderFlow(val counterpartySession: FlowSession) : FlowLogic<Unit>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
subFlow(signTransactionFlow)
|
val idOfTxWeSigned = subFlow(signTransactionFlow).id
|
||||||
// DOCEND 16
|
// DOCEND 16
|
||||||
|
|
||||||
/**-----------------------------
|
/**-----------------------------
|
||||||
@ -658,8 +658,11 @@ class ResponderFlow(val counterpartySession: FlowSession) : FlowLogic<Unit>() {
|
|||||||
-----------------------------**/
|
-----------------------------**/
|
||||||
progressTracker.currentStep = FINALISATION
|
progressTracker.currentStep = FINALISATION
|
||||||
|
|
||||||
// Nothing to do here! As long as some other party calls
|
// As the final step the responder waits to receive the notarised transaction from the sending party
|
||||||
// ``FinalityFlow``, the recording of the transaction on our node
|
// Since it knows the ID of the transaction it just signed, the transaction ID is specified to ensure the correct
|
||||||
// we be handled automatically.
|
// transaction is received and recorded.
|
||||||
|
// DOCSTART ReceiveFinalityFlow
|
||||||
|
subFlow(ReceiveFinalityFlow(counterpartySession, expectedTxId = idOfTxWeSigned))
|
||||||
|
// DOCEND ReceiveFinalityFlow
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -160,7 +160,7 @@ class ForeignExchangeFlow(private val tradeId: String,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initiate the standard protocol to notarise and distribute to the involved parties.
|
// Initiate the standard protocol to notarise and distribute to the involved parties.
|
||||||
subFlow(FinalityFlow(allPartySignedTx, setOf(counterparty)))
|
subFlow(FinalityFlow(allPartySignedTx, counterpartySession))
|
||||||
|
|
||||||
return allPartySignedTx.id
|
return allPartySignedTx.id
|
||||||
}
|
}
|
||||||
@ -239,7 +239,8 @@ class ForeignExchangeRemoteFlow(private val source: FlowSession) : FlowLogic<Uni
|
|||||||
|
|
||||||
// send the other side our signature.
|
// send the other side our signature.
|
||||||
source.send(ourSignature)
|
source.send(ourSignature)
|
||||||
// N.B. The FinalityProtocol will be responsible for Notarising the SignedTransaction
|
|
||||||
// and broadcasting the result to us.
|
// and then finally stored the finalised transaction into our vault
|
||||||
|
subFlow(ReceiveFinalityFlow(source))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,8 +43,11 @@ class IOUFlow(val iouValue: Int,
|
|||||||
// We sign the transaction.
|
// We sign the transaction.
|
||||||
val signedTx = serviceHub.signInitialTransaction(txBuilder)
|
val signedTx = serviceHub.signInitialTransaction(txBuilder)
|
||||||
|
|
||||||
// We finalise the transaction.
|
// Creating a session with the other party.
|
||||||
subFlow(FinalityFlow(signedTx))
|
val otherPartySession = initiateFlow(otherParty)
|
||||||
|
|
||||||
|
// We finalise the transaction and then send it to the counterparty.
|
||||||
|
subFlow(FinalityFlow(signedTx, otherPartySession))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// DOCEND 01
|
// DOCEND 01
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
@file:Suppress("unused")
|
||||||
|
|
||||||
|
package net.corda.docs.kotlin.tutorial.helloworld
|
||||||
|
|
||||||
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
|
import net.corda.core.flows.FlowLogic
|
||||||
|
import net.corda.core.flows.FlowSession
|
||||||
|
import net.corda.core.flows.InitiatedBy
|
||||||
|
import net.corda.core.flows.ReceiveFinalityFlow
|
||||||
|
|
||||||
|
// DOCSTART 01
|
||||||
|
// Replace Responder's definition with:
|
||||||
|
@InitiatedBy(IOUFlow::class)
|
||||||
|
class IOUFlowResponder(private val otherPartySession: FlowSession) : FlowLogic<Unit>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call() {
|
||||||
|
subFlow(ReceiveFinalityFlow(otherPartySession))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// DOCEND 01
|
@ -52,7 +52,7 @@ class IOUFlow(val iouValue: Int,
|
|||||||
val fullySignedTx = subFlow(CollectSignaturesFlow(signedTx, listOf(otherPartySession), CollectSignaturesFlow.tracker()))
|
val fullySignedTx = subFlow(CollectSignaturesFlow(signedTx, listOf(otherPartySession), CollectSignaturesFlow.tracker()))
|
||||||
|
|
||||||
// Finalising the transaction.
|
// Finalising the transaction.
|
||||||
subFlow(FinalityFlow(fullySignedTx))
|
subFlow(FinalityFlow(fullySignedTx, otherPartySession))
|
||||||
// DOCEND 02
|
// DOCEND 02
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,8 @@ import net.corda.core.transactions.SignedTransaction
|
|||||||
class IOUFlowResponder(val otherPartySession: FlowSession) : FlowLogic<Unit>() {
|
class IOUFlowResponder(val otherPartySession: FlowSession) : FlowLogic<Unit>() {
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call() {
|
override fun call() {
|
||||||
val signTransactionFlow = object : SignTransactionFlow(otherPartySession, SignTransactionFlow.tracker()) {
|
// DOCSTART 01
|
||||||
|
val signTransactionFlow = object : SignTransactionFlow(otherPartySession) {
|
||||||
override fun checkTransaction(stx: SignedTransaction) = requireThat {
|
override fun checkTransaction(stx: SignedTransaction) = requireThat {
|
||||||
val output = stx.tx.outputs.single().data
|
val output = stx.tx.outputs.single().data
|
||||||
"This must be an IOU transaction." using (output is IOUState)
|
"This must be an IOU transaction." using (output is IOUState)
|
||||||
@ -26,7 +27,9 @@ class IOUFlowResponder(val otherPartySession: FlowSession) : FlowLogic<Unit>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
subFlow(signTransactionFlow)
|
val expectedTxId = subFlow(signTransactionFlow).id
|
||||||
|
|
||||||
|
subFlow(ReceiveFinalityFlow(otherPartySession, expectedTxId))
|
||||||
|
// DOCEND 01
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// DOCEND 01
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
@file:Suppress("unused")
|
@file:Suppress("unused")
|
||||||
|
|
||||||
package net.corda.docs.kotlin
|
package net.corda.docs.kotlin.txbuild
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
@ -25,7 +25,7 @@ enum class WorkflowState {
|
|||||||
REJECTED
|
REJECTED
|
||||||
}
|
}
|
||||||
|
|
||||||
const val TRADE_APPROVAL_PROGRAM_ID = "net.corda.docs.kotlin.TradeApprovalContract"
|
const val TRADE_APPROVAL_PROGRAM_ID = "net.corda.docs.kotlin.txbuild.TradeApprovalContract"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Minimal contract to encode a simple workflow with one initial state and two possible eventual states.
|
* Minimal contract to encode a simple workflow with one initial state and two possible eventual states.
|
||||||
@ -93,6 +93,7 @@ data class TradeApprovalContract(val blank: Unit? = null) : Contract {
|
|||||||
* The protocol then sends a copy to the other node. We don't require the other party to sign
|
* 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.
|
* as their approval/rejection is to follow.
|
||||||
*/
|
*/
|
||||||
|
@InitiatingFlow
|
||||||
class SubmitTradeApprovalFlow(private val tradeId: String,
|
class SubmitTradeApprovalFlow(private val tradeId: String,
|
||||||
private val counterparty: Party) : FlowLogic<StateAndRef<TradeApprovalContract.State>>() {
|
private val counterparty: Party) : FlowLogic<StateAndRef<TradeApprovalContract.State>>() {
|
||||||
@Suspendable
|
@Suspendable
|
||||||
@ -109,12 +110,20 @@ class SubmitTradeApprovalFlow(private val tradeId: String,
|
|||||||
// We can automatically sign as there is no untrusted data.
|
// We can automatically sign as there is no untrusted data.
|
||||||
val signedTx = serviceHub.signInitialTransaction(tx)
|
val signedTx = serviceHub.signInitialTransaction(tx)
|
||||||
// Notarise and distribute.
|
// Notarise and distribute.
|
||||||
subFlow(FinalityFlow(signedTx, setOf(counterparty)))
|
subFlow(FinalityFlow(signedTx, initiateFlow(counterparty)))
|
||||||
// Return the initial state
|
// Return the initial state
|
||||||
return signedTx.tx.outRef(0)
|
return signedTx.tx.outRef(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@InitiatedBy(SubmitTradeApprovalFlow::class)
|
||||||
|
class SubmitTradeApprovalResponderFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call() {
|
||||||
|
subFlow(ReceiveFinalityFlow(otherSide))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple flow to complete a proposal submitted by another party and ensure both nodes
|
* 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
|
* end up with a fully signed copy of the state either as APPROVED, or REJECTED
|
||||||
@ -174,8 +183,8 @@ class SubmitCompletionFlow(private val ref: StateRef, private val verdict: Workf
|
|||||||
val selfSignedTx = serviceHub.signInitialTransaction(tx)
|
val selfSignedTx = serviceHub.signInitialTransaction(tx)
|
||||||
//DOCEND 2
|
//DOCEND 2
|
||||||
// Send the signed transaction to the originator and await their signature to confirm
|
// Send the signed transaction to the originator and await their signature to confirm
|
||||||
val session = initiateFlow(newState.source)
|
val sourceSession = initiateFlow(newState.source)
|
||||||
val allPartySignedTx = session.sendAndReceive<TransactionSignature>(selfSignedTx).unwrap {
|
val allPartySignedTx = sourceSession.sendAndReceive<TransactionSignature>(selfSignedTx).unwrap {
|
||||||
// Add their signature to our unmodified transaction. To check they signed the same tx.
|
// Add their signature to our unmodified transaction. To check they signed the same tx.
|
||||||
val agreedTx = selfSignedTx + it
|
val agreedTx = selfSignedTx + it
|
||||||
// Receive back their signature and confirm that it is for an unmodified transaction
|
// Receive back their signature and confirm that it is for an unmodified transaction
|
||||||
@ -189,7 +198,7 @@ class SubmitCompletionFlow(private val ref: StateRef, private val verdict: Workf
|
|||||||
}
|
}
|
||||||
// DOCSTART 4
|
// DOCSTART 4
|
||||||
// Notarise and distribute the completed transaction.
|
// Notarise and distribute the completed transaction.
|
||||||
subFlow(FinalityFlow(allPartySignedTx, setOf(newState.source)))
|
subFlow(FinalityFlow(allPartySignedTx, sourceSession))
|
||||||
// DOCEND 4
|
// DOCEND 4
|
||||||
// Return back the details of the completed state/transaction.
|
// Return back the details of the completed state/transaction.
|
||||||
return allPartySignedTx.tx.outRef(0)
|
return allPartySignedTx.tx.outRef(0)
|
||||||
@ -233,7 +242,7 @@ class RecordCompletionFlow(private val sourceSession: FlowSession) : FlowLogic<U
|
|||||||
val ourSignature = serviceHub.createSignature(completeTx)
|
val ourSignature = serviceHub.createSignature(completeTx)
|
||||||
// Send our signature to the other party.
|
// Send our signature to the other party.
|
||||||
sourceSession.send(ourSignature)
|
sourceSession.send(ourSignature)
|
||||||
// N.B. The FinalityProtocol will be responsible for Notarising the SignedTransaction
|
|
||||||
// and broadcasting the result to us.
|
subFlow(ReceiveFinalityFlow(sourceSession))
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
@file:Suppress("unused", "MemberVisibilityCanBePrivate")
|
@file:Suppress("unused", "MemberVisibilityCanBePrivate")
|
||||||
|
|
||||||
package net.corda.docs.kotlin
|
package net.corda.docs.kotlin.vault
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import net.corda.core.contracts.Amount
|
import net.corda.core.contracts.Amount
|
||||||
@ -68,7 +68,6 @@ object CustomVaultQuery {
|
|||||||
* This is a slightly modified version of the IssuerFlow, which uses a 3rd party custom query to
|
* This is a slightly modified version of the IssuerFlow, which uses a 3rd party custom query to
|
||||||
* retrieve a list of currencies and top up amounts to be used in the issuance.
|
* retrieve a list of currencies and top up amounts to be used in the issuance.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
object TopupIssuerFlow {
|
object TopupIssuerFlow {
|
||||||
@CordaSerializable
|
@CordaSerializable
|
||||||
data class TopupRequest(val issueToParty: Party,
|
data class TopupRequest(val issueToParty: Party,
|
@ -1,9 +1,10 @@
|
|||||||
package net.corda.docs.kotlin
|
package net.corda.docs.kotlin.txbuild
|
||||||
|
|
||||||
import net.corda.core.contracts.LinearState
|
import net.corda.core.contracts.LinearState
|
||||||
import net.corda.core.contracts.StateAndRef
|
import net.corda.core.contracts.StateAndRef
|
||||||
import net.corda.core.contracts.UniqueIdentifier
|
import net.corda.core.contracts.UniqueIdentifier
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.internal.packageName
|
||||||
import net.corda.core.node.ServiceHub
|
import net.corda.core.node.ServiceHub
|
||||||
import net.corda.core.node.services.queryBy
|
import net.corda.core.node.services.queryBy
|
||||||
import net.corda.core.node.services.vault.QueryCriteria
|
import net.corda.core.node.services.vault.QueryCriteria
|
||||||
@ -33,7 +34,7 @@ class WorkflowTransactionBuildTutorialTest {
|
|||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.docs"))
|
mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf(javaClass.packageName))
|
||||||
aliceNode = mockNet.createPartyNode(ALICE_NAME)
|
aliceNode = mockNet.createPartyNode(ALICE_NAME)
|
||||||
bobNode = mockNet.createPartyNode(BOB_NAME)
|
bobNode = mockNet.createPartyNode(BOB_NAME)
|
||||||
alice = aliceNode.services.myInfo.identityFromX500Name(ALICE_NAME)
|
alice = aliceNode.services.myInfo.identityFromX500Name(ALICE_NAME)
|
@ -1,13 +1,14 @@
|
|||||||
package net.corda.docs.kotlin
|
package net.corda.docs.kotlin.vault
|
||||||
|
|
||||||
import net.corda.core.contracts.Amount
|
import net.corda.core.contracts.Amount
|
||||||
import net.corda.core.contracts.ContractState
|
import net.corda.core.contracts.ContractState
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.internal.packageName
|
||||||
import net.corda.core.node.services.queryBy
|
import net.corda.core.node.services.queryBy
|
||||||
import net.corda.core.node.services.vault.*
|
import net.corda.core.node.services.vault.*
|
||||||
import net.corda.core.utilities.OpaqueBytes
|
import net.corda.core.utilities.OpaqueBytes
|
||||||
import net.corda.core.utilities.getOrThrow
|
import net.corda.core.utilities.getOrThrow
|
||||||
import net.corda.docs.java.tutorial.helloworld.IOUFlow
|
import net.corda.docs.kotlin.tutorial.helloworld.IOUFlow
|
||||||
import net.corda.finance.*
|
import net.corda.finance.*
|
||||||
import net.corda.finance.contracts.getCashBalances
|
import net.corda.finance.contracts.getCashBalances
|
||||||
import net.corda.finance.flows.CashIssueFlow
|
import net.corda.finance.flows.CashIssueFlow
|
||||||
@ -17,7 +18,7 @@ import net.corda.testing.node.MockNetwork
|
|||||||
import net.corda.testing.node.StartedMockNode
|
import net.corda.testing.node.StartedMockNode
|
||||||
import org.assertj.core.api.Assertions.assertThatCode
|
import org.assertj.core.api.Assertions.assertThatCode
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Assert
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@ -30,7 +31,7 @@ class CustomVaultQueryTest {
|
|||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.finance", "net.corda.docs", "com.template"))
|
mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.finance", IOUFlow::class.packageName, javaClass.packageName, "com.template"))
|
||||||
nodeA = mockNet.createPartyNode()
|
nodeA = mockNet.createPartyNode()
|
||||||
nodeB = mockNet.createPartyNode()
|
nodeB = mockNet.createPartyNode()
|
||||||
notary = mockNet.defaultNotaryIdentity
|
notary = mockNet.defaultNotaryIdentity
|
||||||
@ -43,7 +44,6 @@ class CustomVaultQueryTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `query by max recorded time`() {
|
fun `query by max recorded time`() {
|
||||||
|
|
||||||
nodeA.startFlow(IOUFlow(1000, nodeB.info.singleIdentity())).getOrThrow()
|
nodeA.startFlow(IOUFlow(1000, nodeB.info.singleIdentity())).getOrThrow()
|
||||||
nodeA.startFlow(IOUFlow(500, nodeB.info.singleIdentity())).getOrThrow()
|
nodeA.startFlow(IOUFlow(500, nodeB.info.singleIdentity())).getOrThrow()
|
||||||
|
|
||||||
@ -69,9 +69,9 @@ class CustomVaultQueryTest {
|
|||||||
topUpCurrencies()
|
topUpCurrencies()
|
||||||
val (cashBalancesAfterTopup, _) = getBalances()
|
val (cashBalancesAfterTopup, _) = getBalances()
|
||||||
|
|
||||||
Assert.assertEquals(cashBalancesOriginal[GBP]?.times(2), cashBalancesAfterTopup[GBP])
|
assertEquals(cashBalancesOriginal[GBP]?.times(2), cashBalancesAfterTopup[GBP])
|
||||||
Assert.assertEquals(cashBalancesOriginal[USD]?.times(2) , cashBalancesAfterTopup[USD])
|
assertEquals(cashBalancesOriginal[USD]?.times(2) , cashBalancesAfterTopup[USD])
|
||||||
Assert.assertEquals(cashBalancesOriginal[CHF]?.times( 2), cashBalancesAfterTopup[CHF])
|
assertEquals(cashBalancesOriginal[CHF]?.times( 2), cashBalancesAfterTopup[CHF])
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun issueCashForCurrency(amountToIssue: Amount<Currency>) {
|
private fun issueCashForCurrency(amountToIssue: Amount<Currency>) {
|
||||||
@ -86,7 +86,8 @@ class CustomVaultQueryTest {
|
|||||||
nodeA.info.singleIdentity(),
|
nodeA.info.singleIdentity(),
|
||||||
OpaqueBytes.of(0x01),
|
OpaqueBytes.of(0x01),
|
||||||
nodeA.info.singleIdentity(),
|
nodeA.info.singleIdentity(),
|
||||||
notary)).getOrThrow()
|
notary)
|
||||||
|
).getOrThrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getBalances(): Pair<Map<Currency, Amount<Currency>>, Map<Currency, Amount<Currency>>> {
|
private fun getBalances(): Pair<Map<Currency, Amount<Currency>>, Map<Currency, Amount<Currency>>> {
|
@ -227,8 +227,8 @@ Next, we call another subflow called ``SignTransactionFlow``. ``SignTransactionF
|
|||||||
|
|
||||||
The transaction then needs to be finalized. This is the the process of sending the transaction to a notary to assert
|
The transaction then needs to be finalized. This is the the process of sending the transaction to a notary to assert
|
||||||
(with another signature) that the time-window in the transaction (if any) is valid and there are no double spends.
|
(with another signature) that the time-window in the transaction (if any) is valid and there are no double spends.
|
||||||
In this flow, finalization is handled by the buyer, so we just wait for the signed transaction to appear in our
|
In this flow, finalization is handled by the buyer, we just wait for them to send it to us. It will have the same ID as
|
||||||
transaction storage. It will have the same ID as the one we started with but more signatures.
|
the one we started with but more signatures.
|
||||||
|
|
||||||
Implementing the buyer
|
Implementing the buyer
|
||||||
----------------------
|
----------------------
|
||||||
@ -314,9 +314,11 @@ On the buyer side, we use ``FinalityFlow`` to finalise the transaction. It will:
|
|||||||
* Record the transaction in the local vault, if it is relevant (i.e. involves the owner of the node).
|
* Record the transaction in the local vault, if it is relevant (i.e. involves the owner of the node).
|
||||||
* Send the fully signed transaction to the other participants for recording as well.
|
* Send the fully signed transaction to the other participants for recording as well.
|
||||||
|
|
||||||
.. warning:: If the seller stops before sending the finalised transaction to the buyer, the seller is left with a
|
On the seller side we use ``ReceiveFinalityFlow`` to receive and record the finalised transaction.
|
||||||
valid transaction but the buyer isn't, so they can't spend the asset they just purchased! This sort of thing is not
|
|
||||||
always a risk (as the seller may not gain anything from that sort of behaviour except a lawsuit), but if it is, a future
|
.. warning:: If the buyer stops before sending the finalised transaction to the seller, the buyer is left with a
|
||||||
|
valid transaction but the seller isn't, so they don't get the cash! This sort of thing is not
|
||||||
|
always a risk (as the buyer may not gain anything from that sort of behaviour except a lawsuit), but if it is, a future
|
||||||
version of the platform will allow you to ask the notary to send you the transaction as well, in case your counterparty
|
version of the platform will allow you to ask the notary to send you the transaction as well, in case your counterparty
|
||||||
does not. This is not the default because it reveals more private info to the notary.
|
does not. This is not the default because it reveals more private info to the notary.
|
||||||
|
|
||||||
|
@ -21,11 +21,11 @@ require the following steps:
|
|||||||
|
|
||||||
1. Building the transaction proposal for the issuance of a new IOU onto a ledger
|
1. Building the transaction proposal for the issuance of a new IOU onto a ledger
|
||||||
2. Signing the transaction proposal
|
2. Signing the transaction proposal
|
||||||
3. Recording the transaction
|
3. Recording the transaction and sending it to the IOU's borrower so that they can record it too
|
||||||
4. Sending the transaction to the IOU's borrower so that they can record it too
|
|
||||||
|
|
||||||
At this stage, we do not require the borrower to approve and sign IOU issuance transactions. We will be able to impose
|
We also need the borrower to receive the transaction and record it for itself. At this stage, we do not require the borrower
|
||||||
this requirement when we look at contracts in the next tutorial.
|
to approve and sign IOU issuance transactions. We will be able to impose this requirement when we look at contracts in the
|
||||||
|
next tutorial.
|
||||||
|
|
||||||
Subflows
|
Subflows
|
||||||
^^^^^^^^
|
^^^^^^^^
|
||||||
@ -34,13 +34,11 @@ forcing each developer to reimplement their own logic to handle these tasks, Cor
|
|||||||
to handle these tasks. We call these flows that are invoked in the context of a larger flow to handle a repeatable task
|
to handle these tasks. We call these flows that are invoked in the context of a larger flow to handle a repeatable task
|
||||||
*subflows*.
|
*subflows*.
|
||||||
|
|
||||||
In our case, we can automate steps 3 and 4 of the IOU issuance flow using ``FinalityFlow``.
|
|
||||||
|
|
||||||
FlowLogic
|
FlowLogic
|
||||||
---------
|
---------
|
||||||
All flows must subclass ``FlowLogic``. You then define the steps taken by the flow by overriding ``FlowLogic.call``.
|
All flows must subclass ``FlowLogic``. You then define the steps taken by the flow by overriding ``FlowLogic.call``.
|
||||||
|
|
||||||
Let's define our ``IOUFlow``. Delete the existing ``Responder`` flow. Then replace the definition of ``Initiator`` with the following:
|
Let's define our ``IOUFlow``. Replace the definition of ``Initiator`` with the following:
|
||||||
|
|
||||||
.. container:: codeset
|
.. container:: codeset
|
||||||
|
|
||||||
@ -54,8 +52,9 @@ Let's define our ``IOUFlow``. Delete the existing ``Responder`` flow. Then repla
|
|||||||
:start-after: DOCSTART 01
|
:start-after: DOCSTART 01
|
||||||
:end-before: DOCEND 01
|
:end-before: DOCEND 01
|
||||||
|
|
||||||
If you're following along in Java, you'll also need to rename ``Initiator.java`` to ``IOUFlow.java``. Let's walk
|
If you're following along in Java, you'll also need to rename ``Initiator.java`` to ``IOUFlow.java``.
|
||||||
through this code step-by-step.
|
|
||||||
|
Let's walk through this code step-by-step.
|
||||||
|
|
||||||
We've defined our own ``FlowLogic`` subclass that overrides ``FlowLogic.call``. ``FlowLogic.call`` has a return type
|
We've defined our own ``FlowLogic`` subclass that overrides ``FlowLogic.call``. ``FlowLogic.call`` has a return type
|
||||||
that must match the type parameter passed to ``FlowLogic`` - this is type returned by running the flow.
|
that must match the type parameter passed to ``FlowLogic`` - this is type returned by running the flow.
|
||||||
@ -73,7 +72,7 @@ annotation out will lead to some very weird error messages!
|
|||||||
There are also a few more annotations, on the ``FlowLogic`` subclass itself:
|
There are also a few more annotations, on the ``FlowLogic`` subclass itself:
|
||||||
|
|
||||||
* ``@InitiatingFlow`` means that this flow is part of a flow pair and that it triggers the other side to run the
|
* ``@InitiatingFlow`` means that this flow is part of a flow pair and that it triggers the other side to run the
|
||||||
the counterpart flow.
|
the counterpart flow (which in our case is the ``IOUFlowResponder`` defined below).
|
||||||
* ``@StartableByRPC`` allows the node owner to start this flow via an RPC call
|
* ``@StartableByRPC`` allows the node owner to start this flow via an RPC call
|
||||||
|
|
||||||
Let's walk through the steps of ``FlowLogic.call`` itself. This is where we actually describe the procedure for
|
Let's walk through the steps of ``FlowLogic.call`` itself. This is where we actually describe the procedure for
|
||||||
@ -145,15 +144,36 @@ We sign the transaction using ``ServiceHub.signInitialTransaction``, which retur
|
|||||||
|
|
||||||
Finalising the transaction
|
Finalising the transaction
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
We now have a valid signed transaction. All that's left to do is to have it recorded by all the relevant parties. By
|
We now have a valid signed transaction. All that's left to do is to get the notary to sign it, have that recorded
|
||||||
doing so, it will become a permanent part of the ledger. As discussed, we'll handle this process automatically using a
|
locally and then send it to all the relevant parties. Once that happens the transaction will become a permanent part of the
|
||||||
built-in flow called ``FinalityFlow``. ``FinalityFlow`` completely automates the process of:
|
ledger. We use ``FinalityFlow`` which does all of this for the lender.
|
||||||
|
|
||||||
* Notarising the transaction if required (i.e. if the transaction contains inputs and/or a time-window)
|
For the borrower to receive the transaction they just need a flow that responds to the seller's.
|
||||||
* Recording it in our vault
|
|
||||||
* Sending it to the other participants (i.e. the lender) for them to record as well
|
Creating the borrower's flow
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
The borrower has to use ``ReceiveFinalityFlow`` in order to receive and record the transaction; it needs to respond to
|
||||||
|
the lender's flow. Let's do that by replacing ``Responder`` from the template with the following:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/helloworld/IOUFlowResponder.kt
|
||||||
|
:language: kotlin
|
||||||
|
:start-after: DOCSTART 01
|
||||||
|
:end-before: DOCEND 01
|
||||||
|
|
||||||
|
.. literalinclude:: example-code/src/main/java/net/corda/docs/java/tutorial/helloworld/IOUFlowResponder.java
|
||||||
|
:language: java
|
||||||
|
:start-after: DOCSTART 01
|
||||||
|
:end-before: DOCEND 01
|
||||||
|
|
||||||
|
As with the ``IOUFlow``, our ``IOUFlowResponder`` flow is a ``FlowLogic`` subclass where we've overridden ``FlowLogic.call``.
|
||||||
|
|
||||||
|
The flow is annotated with ``InitiatedBy(IOUFlow.class)``, which means that your node will invoke
|
||||||
|
``IOUFlowResponder.call`` when it receives a message from a instance of ``Initiator`` running on another node. This message
|
||||||
|
will be the finalised transaction which will be recorded in the borrower's vault.
|
||||||
|
|
||||||
Progress so far
|
Progress so far
|
||||||
---------------
|
---------------
|
||||||
Our flow, and our CorDapp, are now ready! We have now defined a flow that we can start on our node to completely
|
Our flow, and our CorDapp, are now ready! We have now defined a flow that we can start on our node to completely
|
||||||
automate the process of issuing an IOU onto the ledger. All that's left is to spin up some nodes and test our CorDapp.
|
automate the process of issuing an IOU onto the ledger. All that's left is to spin up some nodes and test our CorDapp.
|
||||||
|
@ -65,13 +65,10 @@ transaction proposal before finalising it by adding our signature.
|
|||||||
Requesting the borrower's signature
|
Requesting the borrower's signature
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
We now need to communicate with the borrower to request their signature over the transaction. Whenever you want to
|
Previously we wrote a responder flow for the borrower in order to receive the finalised transaction from the lender.
|
||||||
communicate with another party in the context of a flow, you first need to establish a flow session with them. If the
|
We use this same flow to first request their signature over the transaction.
|
||||||
counterparty has a ``FlowLogic`` registered to respond to the ``FlowLogic`` initiating the session, a session will be
|
|
||||||
established. All communication between the two ``FlowLogic`` instances will then place as part of this session.
|
|
||||||
|
|
||||||
Once we have a session with the borrower, we gather the borrower's signature using ``CollectSignaturesFlow``, which
|
We gather the borrower's signature using ``CollectSignaturesFlow``, which takes:
|
||||||
takes:
|
|
||||||
|
|
||||||
* A transaction signed by the flow initiator
|
* A transaction signed by the flow initiator
|
||||||
* A list of flow-sessions between the flow initiator and the required signers
|
* A list of flow-sessions between the flow initiator and the required signers
|
||||||
@ -80,11 +77,11 @@ And returns a transaction signed by all the required signers.
|
|||||||
|
|
||||||
We can then pass this fully-signed transaction into ``FinalityFlow``.
|
We can then pass this fully-signed transaction into ``FinalityFlow``.
|
||||||
|
|
||||||
Creating the borrower's flow
|
Updating the borrower's flow
|
||||||
----------------------------
|
----------------------------
|
||||||
On the lender's side, we used ``CollectSignaturesFlow`` to automate the collection of signatures. To allow the lender
|
On the lender's side, we used ``CollectSignaturesFlow`` to automate the collection of signatures. To allow the borrower
|
||||||
to respond, we need to write a response flow as well. In a new ``IOUFlowResponder.java`` file in Java, or within the
|
to respond, we need to update its responder flow to first receive the partially signed transaction for signing. Update
|
||||||
``App.kt`` file in Kotlin, add the following class:
|
``IOUFlowResponder.call`` to be the following:
|
||||||
|
|
||||||
.. container:: codeset
|
.. container:: codeset
|
||||||
|
|
||||||
@ -92,19 +89,13 @@ to respond, we need to write a response flow as well. In a new ``IOUFlowResponde
|
|||||||
:language: kotlin
|
:language: kotlin
|
||||||
:start-after: DOCSTART 01
|
:start-after: DOCSTART 01
|
||||||
:end-before: DOCEND 01
|
:end-before: DOCEND 01
|
||||||
|
:dedent: 8
|
||||||
|
|
||||||
.. literalinclude:: example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlowResponder.java
|
.. literalinclude:: example-code/src/main/java/net/corda/docs/java/tutorial/twoparty/IOUFlowResponder.java
|
||||||
:language: java
|
:language: java
|
||||||
:start-after: DOCSTART 01
|
:start-after: DOCSTART 01
|
||||||
:end-before: DOCEND 01
|
:end-before: DOCEND 01
|
||||||
|
:dedent: 8
|
||||||
As with the ``IOUFlow``, our ``IOUFlowResponder`` flow is a ``FlowLogic`` subclass where we've overridden
|
|
||||||
``FlowLogic.call``.
|
|
||||||
|
|
||||||
The flow is annotated with ``InitiatedBy(IOUFlow.class)``, which means that your node will invoke
|
|
||||||
``IOUFlowResponder.call`` when it receives a message from a instance of ``Initiator`` running on another node. What
|
|
||||||
will this message from the ``IOUFlow`` be? If we look at the definition of ``CollectSignaturesFlow``, we can see that
|
|
||||||
we'll be sent a ``SignedTransaction``, and are expected to send back our signature over that transaction.
|
|
||||||
|
|
||||||
We could write our own flow to handle this process. However, there is also a pre-defined flow called
|
We could write our own flow to handle this process. However, there is also a pre-defined flow called
|
||||||
``SignTransactionFlow`` that can handle the process automatically. The only catch is that ``SignTransactionFlow`` is an
|
``SignTransactionFlow`` that can handle the process automatically. The only catch is that ``SignTransactionFlow`` is an
|
||||||
@ -128,6 +119,9 @@ signatures are contractually valid.
|
|||||||
Once we've defined the ``SignTransactionFlow`` subclass, we invoke it using ``FlowLogic.subFlow``, and the
|
Once we've defined the ``SignTransactionFlow`` subclass, we invoke it using ``FlowLogic.subFlow``, and the
|
||||||
communication with the borrower's and the lender's flow is conducted automatically.
|
communication with the borrower's and the lender's flow is conducted automatically.
|
||||||
|
|
||||||
|
``SignedTransactionFlow`` returns the newly signed transaction. We pass in the transaction's ID to ``ReceiveFinalityFlow``
|
||||||
|
to ensure we are recording the correct notarised transaction from the lender.
|
||||||
|
|
||||||
Conclusion
|
Conclusion
|
||||||
----------
|
----------
|
||||||
We have now updated our flow to verify the transaction and gather the lender's signature, in line with the constraints
|
We have now updated our flow to verify the transaction and gather the lender's signature, in line with the constraints
|
||||||
|
@ -7,7 +7,7 @@ In the Hello, World tutorial, we built a CorDapp allowing us to model IOUs on le
|
|||||||
elements:
|
elements:
|
||||||
|
|
||||||
* An ``IOUState``, representing IOUs on the blockchain
|
* An ``IOUState``, representing IOUs on the blockchain
|
||||||
* An ``IOUFlow``, orchestrating the process of agreeing the creation of an IOU on-ledger
|
* An ``IOUFlow`` and ``IOFlowResponder`` flow pair, orchestrating the process of agreeing the creation of an IOU on-ledger
|
||||||
|
|
||||||
However, our CorDapp did not impose any constraints on the evolution of IOUs on the blockchain over time. Anyone was free
|
However, our CorDapp did not impose any constraints on the evolution of IOUs on the blockchain over time. Anyone was free
|
||||||
to create IOUs of any value, between any party.
|
to create IOUs of any value, between any party.
|
||||||
|
@ -81,12 +81,10 @@ To give a few more specific details consider two simplified real world
|
|||||||
scenarios. First, a basic foreign exchange cash transaction. This
|
scenarios. First, a basic foreign exchange cash transaction. This
|
||||||
transaction needs to locate a set of funds to exchange. A flow
|
transaction needs to locate a set of funds to exchange. A flow
|
||||||
modelling this is implemented in ``FxTransactionBuildTutorial.kt``
|
modelling this is implemented in ``FxTransactionBuildTutorial.kt``
|
||||||
(see ``docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/FxTransactionBuildTutorial.kt`` in the
|
(in the `main Corda repo <https://github.com/corda/corda>`_).
|
||||||
`main Corda repo <https://github.com/corda/corda>`_).
|
|
||||||
Second, a simple business model in which parties manually accept or
|
Second, a simple business model in which parties manually accept or
|
||||||
reject each other's trade proposals, which is implemented in
|
reject each other's trade proposals, which is implemented in
|
||||||
``WorkflowTransactionBuildTutorial.kt`` (see
|
``WorkflowTransactionBuildTutorial.kt`` (in the
|
||||||
``docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/WorkflowTransactionBuildTutorial.kt`` in the
|
|
||||||
`main Corda repo <https://github.com/corda/corda>`_). To run and explore these
|
`main Corda repo <https://github.com/corda/corda>`_). To run and explore these
|
||||||
examples using the IntelliJ IDE one can run/step through the respective unit
|
examples using the IntelliJ IDE one can run/step through the respective unit
|
||||||
tests in ``FxTransactionBuildTutorialTest.kt`` and
|
tests in ``FxTransactionBuildTutorialTest.kt`` and
|
||||||
@ -148,7 +146,7 @@ parameters to the flow to identify the states being operated upon. Thus
|
|||||||
code to gather the latest input state for a given ``StateRef`` would use
|
code to gather the latest input state for a given ``StateRef`` would use
|
||||||
the ``VaultService`` as follows:
|
the ``VaultService`` as follows:
|
||||||
|
|
||||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/WorkflowTransactionBuildTutorial.kt
|
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/txbuild/WorkflowTransactionBuildTutorial.kt
|
||||||
:language: kotlin
|
:language: kotlin
|
||||||
:start-after: DOCSTART 1
|
:start-after: DOCSTART 1
|
||||||
:end-before: DOCEND 1
|
:end-before: DOCEND 1
|
||||||
@ -221,7 +219,7 @@ and convert it into a ``SignedTransaction``.
|
|||||||
|
|
||||||
Examples of this process are:
|
Examples of this process are:
|
||||||
|
|
||||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/WorkflowTransactionBuildTutorial.kt
|
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/txbuild/WorkflowTransactionBuildTutorial.kt
|
||||||
:language: kotlin
|
:language: kotlin
|
||||||
:start-after: DOCSTART 2
|
:start-after: DOCSTART 2
|
||||||
:end-before: DOCEND 2
|
:end-before: DOCEND 2
|
||||||
@ -260,7 +258,7 @@ 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
|
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:
|
expected for this specific flow. An example of this from the demo code is:
|
||||||
|
|
||||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/WorkflowTransactionBuildTutorial.kt
|
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/txbuild/WorkflowTransactionBuildTutorial.kt
|
||||||
:language: kotlin
|
:language: kotlin
|
||||||
:start-after: DOCSTART 3
|
:start-after: DOCSTART 3
|
||||||
:end-before: DOCEND 3
|
:end-before: DOCEND 3
|
||||||
@ -277,7 +275,7 @@ Once all the signatures are applied to the ``SignedTransaction``, the
|
|||||||
final steps are notarisation and ensuring that all nodes record the fully-signed transaction. The
|
final steps are notarisation and ensuring that all nodes record the fully-signed transaction. The
|
||||||
code for this is standardised in the ``FinalityFlow``:
|
code for this is standardised in the ``FinalityFlow``:
|
||||||
|
|
||||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/WorkflowTransactionBuildTutorial.kt
|
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/txbuild/WorkflowTransactionBuildTutorial.kt
|
||||||
:language: kotlin
|
:language: kotlin
|
||||||
:start-after: DOCSTART 4
|
:start-after: DOCSTART 4
|
||||||
:end-before: DOCEND 4
|
:end-before: DOCEND 4
|
||||||
|
@ -21,13 +21,13 @@ a local network where all the nodes see each other and provides safe shutting do
|
|||||||
|
|
||||||
.. container:: codeset
|
.. container:: codeset
|
||||||
|
|
||||||
.. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/KotlinIntegrationTestingTutorial.kt
|
.. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/kotlin/tutorial/test/KotlinIntegrationTestingTutorial.kt
|
||||||
:language: kotlin
|
:language: kotlin
|
||||||
:start-after: START 1
|
:start-after: START 1
|
||||||
:end-before: END 1
|
:end-before: END 1
|
||||||
:dedent: 8
|
:dedent: 8
|
||||||
|
|
||||||
.. literalinclude:: example-code/src/integration-test/java/net/corda/docs/JavaIntegrationTestingTutorial.java
|
.. literalinclude:: example-code/src/integration-test/java/net/corda/docs/java/tutorial/test/JavaIntegrationTestingTutorial.java
|
||||||
:language: java
|
:language: java
|
||||||
:start-after: START 1
|
:start-after: START 1
|
||||||
:end-before: END 1
|
:end-before: END 1
|
||||||
@ -49,13 +49,13 @@ the information returned; their respective ``NodeHandles`` s.
|
|||||||
|
|
||||||
.. container:: codeset
|
.. container:: codeset
|
||||||
|
|
||||||
.. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/KotlinIntegrationTestingTutorial.kt
|
.. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/kotlin/tutorial/test/KotlinIntegrationTestingTutorial.kt
|
||||||
:language: kotlin
|
:language: kotlin
|
||||||
:start-after: START 2
|
:start-after: START 2
|
||||||
:end-before: END 2
|
:end-before: END 2
|
||||||
:dedent: 12
|
:dedent: 12
|
||||||
|
|
||||||
.. literalinclude:: example-code/src/integration-test/java/net/corda/docs/JavaIntegrationTestingTutorial.java
|
.. literalinclude:: example-code/src/integration-test/java/net/corda/docs/java/tutorial/test/JavaIntegrationTestingTutorial.java
|
||||||
:language: java
|
:language: java
|
||||||
:start-after: START 2
|
:start-after: START 2
|
||||||
:end-before: END 2
|
:end-before: END 2
|
||||||
@ -66,13 +66,13 @@ us to start flows and query state.
|
|||||||
|
|
||||||
.. container:: codeset
|
.. container:: codeset
|
||||||
|
|
||||||
.. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/KotlinIntegrationTestingTutorial.kt
|
.. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/kotlin/tutorial/test/KotlinIntegrationTestingTutorial.kt
|
||||||
:language: kotlin
|
:language: kotlin
|
||||||
:start-after: START 3
|
:start-after: START 3
|
||||||
:end-before: END 3
|
:end-before: END 3
|
||||||
:dedent: 12
|
:dedent: 12
|
||||||
|
|
||||||
.. literalinclude:: example-code/src/integration-test/java/net/corda/docs/JavaIntegrationTestingTutorial.java
|
.. literalinclude:: example-code/src/integration-test/java/net/corda/docs/java/tutorial/test/JavaIntegrationTestingTutorial.java
|
||||||
:language: java
|
:language: java
|
||||||
:start-after: START 3
|
:start-after: START 3
|
||||||
:end-before: END 3
|
:end-before: END 3
|
||||||
@ -84,13 +84,13 @@ Now that we're all set up we can finally get some cash action going!
|
|||||||
|
|
||||||
.. container:: codeset
|
.. container:: codeset
|
||||||
|
|
||||||
.. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/KotlinIntegrationTestingTutorial.kt
|
.. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/kotlin/tutorial/test/KotlinIntegrationTestingTutorial.kt
|
||||||
:language: kotlin
|
:language: kotlin
|
||||||
:start-after: START 4
|
:start-after: START 4
|
||||||
:end-before: END 4
|
:end-before: END 4
|
||||||
:dedent: 12
|
:dedent: 12
|
||||||
|
|
||||||
.. literalinclude:: example-code/src/integration-test/java/net/corda/docs/JavaIntegrationTestingTutorial.java
|
.. literalinclude:: example-code/src/integration-test/java/net/corda/docs/java/tutorial/test/JavaIntegrationTestingTutorial.java
|
||||||
:language: java
|
:language: java
|
||||||
:start-after: START 4
|
:start-after: START 4
|
||||||
:end-before: END 4
|
:end-before: END 4
|
||||||
@ -106,13 +106,13 @@ is asserting.
|
|||||||
|
|
||||||
.. container:: codeset
|
.. container:: codeset
|
||||||
|
|
||||||
.. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/KotlinIntegrationTestingTutorial.kt
|
.. literalinclude:: example-code/src/integration-test/kotlin/net/corda/docs/kotlin/tutorial/test/KotlinIntegrationTestingTutorial.kt
|
||||||
:language: kotlin
|
:language: kotlin
|
||||||
:start-after: START 5
|
:start-after: START 5
|
||||||
:end-before: END 5
|
:end-before: END 5
|
||||||
:dedent: 12
|
:dedent: 12
|
||||||
|
|
||||||
.. literalinclude:: example-code/src/integration-test/java/net/corda/docs/JavaIntegrationTestingTutorial.java
|
.. literalinclude:: example-code/src/integration-test/java/net/corda/docs/java/tutorial/test/JavaIntegrationTestingTutorial.java
|
||||||
:language: java
|
:language: java
|
||||||
:start-after: START 5
|
:start-after: START 5
|
||||||
:end-before: END 5
|
:end-before: END 5
|
||||||
@ -123,6 +123,6 @@ Next we want Bob to send this cash back to Alice.
|
|||||||
That's it! We saw how to start up several corda nodes locally, how to connect to them, and how to test some simple invariants
|
That's it! We saw how to start up several corda nodes locally, how to connect to them, and how to test some simple invariants
|
||||||
about ``CashIssueAndPaymentFlow`` and ``CashPaymentFlow``.
|
about ``CashIssueAndPaymentFlow`` and ``CashPaymentFlow``.
|
||||||
|
|
||||||
You can find the complete test at ``example-code/src/integration-test/java/net/corda/docs/JavaIntegrationTestingTutorial.java``
|
You can find the complete test at ``example-code/src/integration-test/java/net/corda/docs/java/tutorial/test/JavaIntegrationTestingTutorial.java``
|
||||||
(Java) and ``example-code/src/integration-test/kotlin/net/corda/docs/KotlinIntegrationTestingTutorial.kt`` (Kotlin) in the
|
(Java) and ``example-code/src/integration-test/kotlin/net/corda/docs/kotlin/tutorial/test/KotlinIntegrationTestingTutorial.kt`` (Kotlin) in the
|
||||||
`Corda repo <https://github.com/corda/corda>`_.
|
`Corda repo <https://github.com/corda/corda>`_.
|
||||||
|
@ -1,3 +1,9 @@
|
|||||||
|
.. highlight:: kotlin
|
||||||
|
.. raw:: html
|
||||||
|
|
||||||
|
<script type="text/javascript" src="_static/jquery.js"></script>
|
||||||
|
<script type="text/javascript" src="_static/codesets.js"></script>
|
||||||
|
|
||||||
Upgrading a CorDapp to a new platform version
|
Upgrading a CorDapp to a new platform version
|
||||||
=============================================
|
=============================================
|
||||||
|
|
||||||
@ -34,22 +40,121 @@ do this by connecting directly to the node's ``persistence.mv.db`` file. See :re
|
|||||||
UNRELEASED
|
UNRELEASED
|
||||||
----------
|
----------
|
||||||
|
|
||||||
* Database upgrade - Change the type of the ``checkpoint_value``.
|
FinalityFlow
|
||||||
This will address the issue that the `vacuum` function is unable to clean up deleted checkpoints as they are still referenced from the ``pg_shdepend`` table.
|
^^^^^^^^^^^^
|
||||||
|
|
||||||
|
The previous ``FinalityFlow`` API is insecure. It requires a handler flow in the counterparty node which accepts any and
|
||||||
|
all signed transactions that are sent to it, without checks. It is **highly** recommended that existing CorDapps migrate
|
||||||
|
away to the new API.
|
||||||
|
|
||||||
|
As an example, let's take a very simple flow that finalises a transaction without the involvement of a counterpart flow:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/FinalityFlowMigration.kt
|
||||||
|
:language: kotlin
|
||||||
|
:start-after: DOCSTART SimpleFlowUsingOldApi
|
||||||
|
:end-before: DOCEND SimpleFlowUsingOldApi
|
||||||
|
|
||||||
|
.. literalinclude:: example-code/src/main/java/net/corda/docs/java/FinalityFlowMigration.java
|
||||||
|
:language: java
|
||||||
|
:start-after: DOCSTART SimpleFlowUsingOldApi
|
||||||
|
:end-before: DOCEND SimpleFlowUsingOldApi
|
||||||
|
:dedent: 4
|
||||||
|
|
||||||
|
To use the new API, this flow needs to be annotated with ``InitiatingFlow`` and a ``FlowSession`` to the participant of the transaction must be
|
||||||
|
passed to ``FinalityFlow`` :
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/FinalityFlowMigration.kt
|
||||||
|
:language: kotlin
|
||||||
|
:start-after: DOCSTART SimpleFlowUsingNewApi
|
||||||
|
:end-before: DOCEND SimpleFlowUsingNewApi
|
||||||
|
|
||||||
|
.. literalinclude:: example-code/src/main/java/net/corda/docs/java/FinalityFlowMigration.java
|
||||||
|
:language: java
|
||||||
|
:start-after: DOCSTART SimpleFlowUsingNewApi
|
||||||
|
:end-before: DOCEND SimpleFlowUsingNewApi
|
||||||
|
:dedent: 4
|
||||||
|
|
||||||
|
If there are more than one transaction participants then a session to each one must be initiated, excluding the local party
|
||||||
|
and the notary.
|
||||||
|
|
||||||
|
A responder flow has to be introduced, which will automatically run on the other participants' nodes, which will call ``ReceiveFinalityFlow``
|
||||||
|
to record the finalised transaction:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/FinalityFlowMigration.kt
|
||||||
|
:language: kotlin
|
||||||
|
:start-after: DOCSTART SimpleNewResponderFlow
|
||||||
|
:end-before: DOCEND SimpleNewResponderFlow
|
||||||
|
|
||||||
|
.. literalinclude:: example-code/src/main/java/net/corda/docs/java/FinalityFlowMigration.java
|
||||||
|
:language: java
|
||||||
|
:start-after: DOCSTART SimpleNewResponderFlow
|
||||||
|
:end-before: DOCEND SimpleNewResponderFlow
|
||||||
|
:dedent: 4
|
||||||
|
|
||||||
|
For flows which are already initiating counterpart flows then it's a simple matter of using the existing flow session.
|
||||||
|
Note however, the new ``FinalityFlow`` is inlined and so the sequence of sends and receives between the two flows will
|
||||||
|
change and will be incompatible with your current flows. You can use the flow version API to write your flows in a
|
||||||
|
backwards compatible way.
|
||||||
|
|
||||||
|
Here's what an upgraded initiating flow may look like:
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/FinalityFlowMigration.kt
|
||||||
|
:language: kotlin
|
||||||
|
:start-after: DOCSTART ExistingInitiatingFlow
|
||||||
|
:end-before: DOCEND ExistingInitiatingFlow
|
||||||
|
|
||||||
|
.. literalinclude:: example-code/src/main/java/net/corda/docs/java/FinalityFlowMigration.java
|
||||||
|
:language: java
|
||||||
|
:start-after: DOCSTART ExistingInitiatingFlow
|
||||||
|
:end-before: DOCEND ExistingInitiatingFlow
|
||||||
|
:dedent: 4
|
||||||
|
|
||||||
|
For the responder flow, insert a call to ``ReceiveFinalityFlow`` at the location where it's expecting to receive the
|
||||||
|
finalised transaction. If the initiator is written in a backwards compatible way then so must the responder.
|
||||||
|
|
||||||
|
.. container:: codeset
|
||||||
|
|
||||||
|
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/FinalityFlowMigration.kt
|
||||||
|
:language: kotlin
|
||||||
|
:start-after: DOCSTART ExistingResponderFlow
|
||||||
|
:end-before: DOCEND ExistingResponderFlow
|
||||||
|
:dedent: 8
|
||||||
|
|
||||||
|
.. literalinclude:: example-code/src/main/java/net/corda/docs/java/FinalityFlowMigration.java
|
||||||
|
:language: java
|
||||||
|
:start-after: DOCSTART ExistingResponderFlow
|
||||||
|
:end-before: DOCEND ExistingResponderFlow
|
||||||
|
:dedent: 12
|
||||||
|
|
||||||
|
The responder flow may be waiting for the finalised transaction to appear in the local node's vault using ``waitForLedgerCommit``.
|
||||||
|
This is no longer necessary with ``ReceiveFinalityFlow`` and the call to ``waitForLedgerCommit`` can be removed.
|
||||||
|
|
||||||
|
Database schema changes
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
The type of the ``checkpoint_value`` column has changed. This will address the issue that the `vacuum` function is unable
|
||||||
|
to clean up deleted checkpoints as they are still referenced from the ``pg_shdepend`` table.
|
||||||
|
|
||||||
For Postgres:
|
For Postgres:
|
||||||
|
|
||||||
.. sourcecode:: sql
|
.. sourcecode:: sql
|
||||||
|
|
||||||
ALTER TABLE node_checkpoints ALTER COLUMN checkpoint_value set data type bytea;
|
ALTER TABLE node_checkpoints ALTER COLUMN checkpoint_value set data type bytea;
|
||||||
|
|
||||||
For H2:
|
For H2:
|
||||||
|
|
||||||
.. sourcecode:: sql
|
.. sourcecode:: sql
|
||||||
|
|
||||||
ALTER TABLE node_checkpoints ALTER COLUMN checkpoint_value set data type VARBINARY(33554432);
|
ALTER TABLE node_checkpoints ALTER COLUMN checkpoint_value set data type VARBINARY(33554432);
|
||||||
|
|
||||||
|
|
||||||
* API change: ``net.corda.core.schemas.PersistentStateRef`` fields (``index`` and ``txId``) incorrectly marked as nullable are now non-nullable,
|
* API change: ``net.corda.core.schemas.PersistentStateRef`` fields (``index`` and ``txId``) incorrectly marked as nullable are now non-nullable,
|
||||||
:doc:`changelog` contains the explanation.
|
:doc:`changelog` contains the explanation.
|
||||||
|
|
||||||
|
@ -19,16 +19,6 @@ inline fun <reified T : ContractState> ServiceHub.queryStateByRef(ref: StateRef)
|
|||||||
return results.states.firstOrNull() ?: throw IllegalArgumentException("State (type=${T::class}) corresponding to the reference $ref not found (or is spent).")
|
return results.states.firstOrNull() ?: throw IllegalArgumentException("State (type=${T::class}) corresponding to the reference $ref not found (or is spent).")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Shorthand when a single party signs a TX and then returns a result that uses the signed TX (e.g. includes the TX id)
|
|
||||||
*/
|
|
||||||
@Suspendable
|
|
||||||
fun <R> FlowLogic<R>.finalize(tx: TransactionBuilder, returnWithSignedTx: (stx: SignedTransaction) -> R): R {
|
|
||||||
val stx = serviceHub.signInitialTransaction(tx)
|
|
||||||
subFlow(FinalityFlow(stx)) // it'll send to all participants in the state by default
|
|
||||||
return returnWithSignedTx(stx)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Corda fails when it tries to store the same attachment hash twice. And it's convenient to also do nothing if no attachment is provided.
|
* Corda fails when it tries to store the same attachment hash twice. And it's convenient to also do nothing if no attachment is provided.
|
||||||
* This doesn't fix the same-attachment problem completely but should at least help in testing with the same file.
|
* This doesn't fix the same-attachment problem completely but should at least help in testing with the same file.
|
||||||
|
@ -2,12 +2,8 @@ package net.corda.finance.flows
|
|||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import net.corda.core.contracts.Amount
|
import net.corda.core.contracts.Amount
|
||||||
import net.corda.core.flows.FinalityFlow
|
import net.corda.core.flows.*
|
||||||
import net.corda.core.flows.FlowException
|
|
||||||
import net.corda.core.flows.FlowLogic
|
|
||||||
import net.corda.core.flows.NotaryException
|
|
||||||
import net.corda.core.identity.AbstractParty
|
import net.corda.core.identity.AbstractParty
|
||||||
import net.corda.core.identity.Party
|
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.utilities.ProgressTracker
|
import net.corda.core.utilities.ProgressTracker
|
||||||
@ -27,9 +23,9 @@ abstract class AbstractCashFlow<out T>(override val progressTracker: ProgressTra
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
protected fun finaliseTx(tx: SignedTransaction, extraParticipants: Set<Party>, message: String): SignedTransaction {
|
protected fun finaliseTx(tx: SignedTransaction, sessions: Collection<FlowSession>, message: String): SignedTransaction {
|
||||||
try {
|
try {
|
||||||
return subFlow(FinalityFlow(tx, extraParticipants))
|
return subFlow(FinalityFlow(tx, sessions))
|
||||||
} catch (e: NotaryException) {
|
} catch (e: NotaryException) {
|
||||||
throw CashException(message, e)
|
throw CashException(message, e)
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,7 @@ package net.corda.finance.flows
|
|||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import net.corda.core.contracts.Amount
|
import net.corda.core.contracts.Amount
|
||||||
import net.corda.core.contracts.InsufficientBalanceException
|
import net.corda.core.contracts.InsufficientBalanceException
|
||||||
import net.corda.core.flows.StartableByRPC
|
import net.corda.core.flows.*
|
||||||
import net.corda.core.identity.Party
|
|
||||||
import net.corda.core.node.services.queryBy
|
import net.corda.core.node.services.queryBy
|
||||||
import net.corda.core.node.services.vault.DEFAULT_PAGE_NUM
|
import net.corda.core.node.services.vault.DEFAULT_PAGE_NUM
|
||||||
import net.corda.core.node.services.vault.PageSpecification
|
import net.corda.core.node.services.vault.PageSpecification
|
||||||
@ -28,6 +27,7 @@ import java.util.*
|
|||||||
* @param issuerRef the reference on the issued currency. Added to the node's legal identity to determine the
|
* @param issuerRef the reference on the issued currency. Added to the node's legal identity to determine the
|
||||||
* issuer.
|
* issuer.
|
||||||
*/
|
*/
|
||||||
|
@InitiatingFlow
|
||||||
@StartableByRPC
|
@StartableByRPC
|
||||||
class CashExitFlow(private val amount: Amount<Currency>,
|
class CashExitFlow(private val amount: Amount<Currency>,
|
||||||
private val issuerRef: OpaqueBytes,
|
private val issuerRef: OpaqueBytes,
|
||||||
@ -53,7 +53,7 @@ class CashExitFlow(private val amount: Amount<Currency>,
|
|||||||
.getInstance { serviceHub.jdbcSession().metaData }
|
.getInstance { serviceHub.jdbcSession().metaData }
|
||||||
.unconsumedCashStatesForSpending(serviceHub, amount, setOf(issuer.party), builder.notary, builder.lockId, setOf(issuer.reference))
|
.unconsumedCashStatesForSpending(serviceHub, amount, setOf(issuer.party), builder.notary, builder.lockId, setOf(issuer.reference))
|
||||||
val signers = try {
|
val signers = try {
|
||||||
val changeOwner = exitStates.map { it.state.data.owner }.toSet().firstOrNull() ?: throw InsufficientBalanceException(amount)
|
val changeOwner = exitStates.asSequence().map { it.state.data.owner }.toSet().firstOrNull() ?: throw InsufficientBalanceException(amount)
|
||||||
Cash().generateExit(
|
Cash().generateExit(
|
||||||
builder,
|
builder,
|
||||||
amount.issuedBy(issuer),
|
amount.issuedBy(issuer),
|
||||||
@ -67,21 +67,31 @@ class CashExitFlow(private val amount: Amount<Currency>,
|
|||||||
val inputStates = serviceHub.vaultService.queryBy<Cash.State>(VaultQueryCriteria(stateRefs = builder.inputStates()),
|
val inputStates = serviceHub.vaultService.queryBy<Cash.State>(VaultQueryCriteria(stateRefs = builder.inputStates()),
|
||||||
PageSpecification(pageNumber = DEFAULT_PAGE_NUM, pageSize = builder.inputStates().size)).states
|
PageSpecification(pageNumber = DEFAULT_PAGE_NUM, pageSize = builder.inputStates().size)).states
|
||||||
|
|
||||||
// TODO: Is it safe to drop participants we don't know how to contact? Does not knowing how to contact them
|
val participantSessions = inputStates
|
||||||
// count as a reason to fail?
|
.asSequence()
|
||||||
val participants: Set<Party> = inputStates
|
|
||||||
.mapNotNull { serviceHub.identityService.wellKnownPartyFromAnonymous(it.state.data.owner) }
|
.mapNotNull { serviceHub.identityService.wellKnownPartyFromAnonymous(it.state.data.owner) }
|
||||||
.toSet()
|
.filterNot(serviceHub.myInfo::isLegalIdentity)
|
||||||
|
.distinct()
|
||||||
|
.map(::initiateFlow)
|
||||||
|
.toList()
|
||||||
// Sign transaction
|
// Sign transaction
|
||||||
progressTracker.currentStep = SIGNING_TX
|
progressTracker.currentStep = SIGNING_TX
|
||||||
val tx = serviceHub.signInitialTransaction(builder, signers)
|
val tx = serviceHub.signInitialTransaction(builder, signers)
|
||||||
|
|
||||||
// Commit the transaction
|
// Commit the transaction
|
||||||
progressTracker.currentStep = FINALISING_TX
|
progressTracker.currentStep = FINALISING_TX
|
||||||
val notarised = finaliseTx(tx, participants, "Unable to notarise exit")
|
val notarised = finaliseTx(tx, participantSessions, "Unable to notarise exit")
|
||||||
return Result(notarised, null)
|
return Result(notarised, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
@CordaSerializable
|
@CordaSerializable
|
||||||
class ExitRequest(amount: Amount<Currency>, val issuerRef: OpaqueBytes) : AbstractRequest(amount)
|
class ExitRequest(amount: Amount<Currency>, val issuerRef: OpaqueBytes) : AbstractRequest(amount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@InitiatedBy(CashExitFlow::class)
|
||||||
|
class CashExitResponderFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call() {
|
||||||
|
subFlow(ReceiveFinalityFlow(otherSide))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -4,7 +4,7 @@ import co.paralleluniverse.fibers.Suspendable
|
|||||||
import net.corda.confidential.SwapIdentitiesFlow
|
import net.corda.confidential.SwapIdentitiesFlow
|
||||||
import net.corda.core.contracts.Amount
|
import net.corda.core.contracts.Amount
|
||||||
import net.corda.core.contracts.InsufficientBalanceException
|
import net.corda.core.contracts.InsufficientBalanceException
|
||||||
import net.corda.core.flows.StartableByRPC
|
import net.corda.core.flows.*
|
||||||
import net.corda.core.identity.AnonymousParty
|
import net.corda.core.identity.AnonymousParty
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.serialization.CordaSerializable
|
import net.corda.core.serialization.CordaSerializable
|
||||||
@ -28,6 +28,7 @@ import java.util.*
|
|||||||
* for testing purposes.
|
* for testing purposes.
|
||||||
*/
|
*/
|
||||||
@StartableByRPC
|
@StartableByRPC
|
||||||
|
@InitiatingFlow
|
||||||
open class CashPaymentFlow(
|
open class CashPaymentFlow(
|
||||||
val amount: Amount<Currency>,
|
val amount: Amount<Currency>,
|
||||||
val recipient: Party,
|
val recipient: Party,
|
||||||
@ -75,7 +76,8 @@ open class CashPaymentFlow(
|
|||||||
|
|
||||||
progressTracker.currentStep = FINALISING_TX
|
progressTracker.currentStep = FINALISING_TX
|
||||||
logger.info("Finalising transaction for: ${tx.id}")
|
logger.info("Finalising transaction for: ${tx.id}")
|
||||||
val notarised = finaliseTx(tx, setOf(recipient), "Unable to notarise spend")
|
val sessions = if (serviceHub.myInfo.isLegalIdentity(recipient)) emptyList() else listOf(initiateFlow(recipient))
|
||||||
|
val notarised = finaliseTx(tx, sessions, "Unable to notarise spend")
|
||||||
logger.info("Finalised transaction for: ${notarised.id}")
|
logger.info("Finalised transaction for: ${notarised.id}")
|
||||||
return Result(notarised, anonymousRecipient)
|
return Result(notarised, anonymousRecipient)
|
||||||
}
|
}
|
||||||
@ -87,3 +89,11 @@ open class CashPaymentFlow(
|
|||||||
val issuerConstraint: Set<Party> = emptySet(),
|
val issuerConstraint: Set<Party> = emptySet(),
|
||||||
val notary: Party? = null) : AbstractRequest(amount)
|
val notary: Party? = null) : AbstractRequest(amount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@InitiatedBy(CashPaymentFlow::class)
|
||||||
|
class CashPaymentResponderFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call() {
|
||||||
|
subFlow(ReceiveFinalityFlow(otherSide))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -71,7 +71,7 @@ object TwoPartyDealFlow {
|
|||||||
|
|
||||||
val txId = subFlow(signTransactionFlow).id
|
val txId = subFlow(signTransactionFlow).id
|
||||||
|
|
||||||
return waitForLedgerCommit(txId)
|
return subFlow(ReceiveFinalityFlow(otherSideSession, expectedTxId = txId))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
@ -81,8 +81,7 @@ object TwoPartyDealFlow {
|
|||||||
/**
|
/**
|
||||||
* Abstracted bilateral deal flow participant that is recipient of initial communication.
|
* Abstracted bilateral deal flow participant that is recipient of initial communication.
|
||||||
*/
|
*/
|
||||||
abstract class Secondary<U>(override val progressTracker: ProgressTracker = Secondary.tracker(),
|
abstract class Secondary<U>(override val progressTracker: ProgressTracker = Secondary.tracker()) : FlowLogic<SignedTransaction>() {
|
||||||
val regulators: Set<Party> = emptySet()) : FlowLogic<SignedTransaction>() {
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
object RECEIVING : ProgressTracker.Step("Waiting for deal info.")
|
object RECEIVING : ProgressTracker.Step("Waiting for deal info.")
|
||||||
@ -124,7 +123,7 @@ object TwoPartyDealFlow {
|
|||||||
logger.trace("Got signatures from other party, verifying ... ")
|
logger.trace("Got signatures from other party, verifying ... ")
|
||||||
|
|
||||||
progressTracker.currentStep = RECORDING
|
progressTracker.currentStep = RECORDING
|
||||||
val ftx = subFlow(FinalityFlow(stx, regulators + otherSideSession.counterparty))
|
val ftx = subFlow(FinalityFlow(stx, otherSideSession))
|
||||||
logger.trace("Recorded transaction.")
|
logger.trace("Recorded transaction.")
|
||||||
|
|
||||||
return ftx
|
return ftx
|
||||||
|
@ -114,7 +114,7 @@ object TwoPartyTradeFlow {
|
|||||||
val txId = subFlow(signTransactionFlow).id
|
val txId = subFlow(signTransactionFlow).id
|
||||||
// DOCEND 5
|
// DOCEND 5
|
||||||
|
|
||||||
return waitForLedgerCommit(txId)
|
return subFlow(ReceiveFinalityFlow(otherSideSession, expectedTxId = txId))
|
||||||
}
|
}
|
||||||
// DOCEND 4
|
// DOCEND 4
|
||||||
|
|
||||||
@ -188,7 +188,7 @@ object TwoPartyTradeFlow {
|
|||||||
|
|
||||||
// Notarise and record the transaction.
|
// Notarise and record the transaction.
|
||||||
progressTracker.currentStep = RECORDING
|
progressTracker.currentStep = RECORDING
|
||||||
return subFlow(FinalityFlow(twiceSignedTx))
|
return subFlow(FinalityFlow(twiceSignedTx, sellerSession))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
|
@ -13,6 +13,7 @@ import net.corda.testing.core.*
|
|||||||
import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin
|
import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin
|
||||||
import net.corda.testing.node.MockNetwork
|
import net.corda.testing.node.MockNetwork
|
||||||
import net.corda.testing.node.StartedMockNode
|
import net.corda.testing.node.StartedMockNode
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@ -29,7 +30,7 @@ class CashPaymentFlowTests {
|
|||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun start() {
|
fun start() {
|
||||||
mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin(), cordappPackages = listOf("net.corda.finance.contracts.asset", "net.corda.finance.schemas"))
|
mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin(), cordappPackages = listOf("net.corda.finance"))
|
||||||
bankOfCordaNode = mockNet.createPartyNode(BOC_NAME)
|
bankOfCordaNode = mockNet.createPartyNode(BOC_NAME)
|
||||||
bankOfCorda = bankOfCordaNode.info.identityFromX500Name(BOC_NAME)
|
bankOfCorda = bankOfCordaNode.info.identityFromX500Name(BOC_NAME)
|
||||||
aliceNode = mockNet.createPartyNode(ALICE_NAME)
|
aliceNode = mockNet.createPartyNode(ALICE_NAME)
|
||||||
@ -61,10 +62,10 @@ class CashPaymentFlowTests {
|
|||||||
// Check Bank of Corda vault updates - we take in some issued cash and split it into $500 to the notary
|
// Check Bank of Corda vault updates - we take in some issued cash and split it into $500 to the notary
|
||||||
// and $1,500 back to us, so we expect to consume one state, produce one state for our own vault
|
// and $1,500 back to us, so we expect to consume one state, produce one state for our own vault
|
||||||
vaultUpdatesBoc.expectEvents {
|
vaultUpdatesBoc.expectEvents {
|
||||||
expect { update ->
|
expect { (consumed, produced) ->
|
||||||
require(update.consumed.size == 1) { "Expected 1 consumed states, actual: $update" }
|
assertThat(consumed).hasSize(1)
|
||||||
require(update.produced.size == 1) { "Expected 1 produced states, actual: $update" }
|
assertThat(produced).hasSize(1)
|
||||||
val changeState = update.produced.single().state.data
|
val changeState = produced.single().state.data
|
||||||
assertEquals(expectedChange.`issued by`(bankOfCorda.ref(ref)), changeState.amount)
|
assertEquals(expectedChange.`issued by`(bankOfCorda.ref(ref)), changeState.amount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -72,8 +73,8 @@ class CashPaymentFlowTests {
|
|||||||
// Check notary node vault updates
|
// Check notary node vault updates
|
||||||
vaultUpdatesBankClient.expectEvents {
|
vaultUpdatesBankClient.expectEvents {
|
||||||
expect { (consumed, produced) ->
|
expect { (consumed, produced) ->
|
||||||
require(consumed.isEmpty()) { consumed.size }
|
assertThat(consumed).isEmpty()
|
||||||
require(produced.size == 1) { produced.size }
|
assertThat(produced).hasSize(1)
|
||||||
val paymentState = produced.single().state.data
|
val paymentState = produced.single().state.data
|
||||||
assertEquals(expectedPayment.`issued by`(bankOfCorda.ref(ref)), paymentState.amount)
|
assertEquals(expectedPayment.`issued by`(bankOfCorda.ref(ref)), paymentState.amount)
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,6 @@ import java.util.concurrent.Executors
|
|||||||
import java.util.concurrent.ScheduledExecutorService
|
import java.util.concurrent.ScheduledExecutorService
|
||||||
|
|
||||||
class FlowsDrainingModeContentionTest {
|
class FlowsDrainingModeContentionTest {
|
||||||
|
|
||||||
private val portAllocation = PortAllocation.Incremental(10000)
|
private val portAllocation = PortAllocation.Incremental(10000)
|
||||||
private val user = User("mark", "dadada", setOf(all()))
|
private val user = User("mark", "dadada", setOf(all()))
|
||||||
private val users = listOf(user)
|
private val users = listOf(user)
|
||||||
@ -90,7 +89,7 @@ class ProposeTransactionAndWaitForCommit(private val data: String,
|
|||||||
subFlow(SendTransactionFlow(session, signedTx))
|
subFlow(SendTransactionFlow(session, signedTx))
|
||||||
session.send(myRpcInfo)
|
session.send(myRpcInfo)
|
||||||
|
|
||||||
return waitForLedgerCommit(signedTx.id)
|
return subFlow(ReceiveFinalityFlow(session, expectedTxId = signedTx.id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,7 +103,7 @@ class SignTransactionTriggerDrainingModeAndFinality(private val session: FlowSes
|
|||||||
|
|
||||||
triggerDrainingModeForInitiatingNode(initiatingRpcInfo)
|
triggerDrainingModeForInitiatingNode(initiatingRpcInfo)
|
||||||
|
|
||||||
subFlow(FinalityFlow(signedTx, setOf(session.counterparty)))
|
subFlow(FinalityFlow(signedTx, session))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun triggerDrainingModeForInitiatingNode(initiatingRpcInfo: RpcInfo) {
|
private fun triggerDrainingModeForInitiatingNode(initiatingRpcInfo: RpcInfo) {
|
||||||
|
@ -136,6 +136,6 @@ class SendMessageFlow(private val message: Message, private val notary: Party) :
|
|||||||
val signedTx = serviceHub.signInitialTransaction(txBuilder)
|
val signedTx = serviceHub.signInitialTransaction(txBuilder)
|
||||||
|
|
||||||
progressTracker.currentStep = FINALISING_TRANSACTION
|
progressTracker.currentStep = FINALISING_TRANSACTION
|
||||||
return subFlow(FinalityFlow(signedTx, FINALISING_TRANSACTION.childProgressTracker()))
|
return subFlow(FinalityFlow(signedTx, emptyList(), FINALISING_TRANSACTION.childProgressTracker()))
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -4,9 +4,7 @@ import co.paralleluniverse.fibers.Suspendable
|
|||||||
import com.google.common.collect.ImmutableList
|
import com.google.common.collect.ImmutableList
|
||||||
import net.corda.client.rpc.CordaRPCClient
|
import net.corda.client.rpc.CordaRPCClient
|
||||||
import net.corda.core.concurrent.CordaFuture
|
import net.corda.core.concurrent.CordaFuture
|
||||||
import net.corda.core.flows.FinalityFlow
|
import net.corda.core.flows.*
|
||||||
import net.corda.core.flows.FlowLogic
|
|
||||||
import net.corda.core.flows.StartableByRPC
|
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.internal.concurrent.transpose
|
import net.corda.core.internal.concurrent.transpose
|
||||||
import net.corda.core.messaging.startFlow
|
import net.corda.core.messaging.startFlow
|
||||||
@ -32,6 +30,7 @@ import kotlin.test.assertEquals
|
|||||||
|
|
||||||
class ScheduledFlowIntegrationTests {
|
class ScheduledFlowIntegrationTests {
|
||||||
@StartableByRPC
|
@StartableByRPC
|
||||||
|
@InitiatingFlow
|
||||||
class InsertInitialStateFlow(private val destination: Party,
|
class InsertInitialStateFlow(private val destination: Party,
|
||||||
private val notary: Party,
|
private val notary: Party,
|
||||||
private val identity: Int = 1,
|
private val identity: Int = 1,
|
||||||
@ -44,11 +43,20 @@ class ScheduledFlowIntegrationTests {
|
|||||||
.addOutputState(scheduledState, DummyContract.PROGRAM_ID)
|
.addOutputState(scheduledState, DummyContract.PROGRAM_ID)
|
||||||
.addCommand(dummyCommand(ourIdentity.owningKey))
|
.addCommand(dummyCommand(ourIdentity.owningKey))
|
||||||
val tx = serviceHub.signInitialTransaction(builder)
|
val tx = serviceHub.signInitialTransaction(builder)
|
||||||
subFlow(FinalityFlow(tx))
|
subFlow(FinalityFlow(tx, initiateFlow(destination)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@InitiatedBy(InsertInitialStateFlow::class)
|
||||||
|
class InsertInitialStateResponderFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call() {
|
||||||
|
subFlow(ReceiveFinalityFlow(otherSide))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@StartableByRPC
|
@StartableByRPC
|
||||||
|
@InitiatingFlow
|
||||||
class AnotherFlow(private val identity: String) : FlowLogic<Unit>() {
|
class AnotherFlow(private val identity: String) : FlowLogic<Unit>() {
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call() {
|
override fun call() {
|
||||||
@ -64,7 +72,15 @@ class ScheduledFlowIntegrationTests {
|
|||||||
.addOutputState(outputState, DummyContract.PROGRAM_ID)
|
.addOutputState(outputState, DummyContract.PROGRAM_ID)
|
||||||
.addCommand(dummyCommand(ourIdentity.owningKey))
|
.addCommand(dummyCommand(ourIdentity.owningKey))
|
||||||
val tx = serviceHub.signInitialTransaction(builder)
|
val tx = serviceHub.signInitialTransaction(builder)
|
||||||
subFlow(FinalityFlow(tx, outputState.participants.toSet()))
|
subFlow(FinalityFlow(tx, initiateFlow(state.state.data.destination)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@InitiatedBy(AnotherFlow::class)
|
||||||
|
class AnotherResponderFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call() {
|
||||||
|
subFlow(ReceiveFinalityFlow(otherSide))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,10 +2,7 @@ package net.corda.testMessage
|
|||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
import net.corda.core.flows.FinalityFlow
|
import net.corda.core.flows.*
|
||||||
import net.corda.core.flows.FlowLogic
|
|
||||||
import net.corda.core.flows.FlowLogicRefFactory
|
|
||||||
import net.corda.core.flows.SchedulableFlow
|
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
import net.corda.core.utilities.NonEmptySet
|
import net.corda.core.utilities.NonEmptySet
|
||||||
@ -16,6 +13,7 @@ import java.util.*
|
|||||||
import kotlin.reflect.jvm.jvmName
|
import kotlin.reflect.jvm.jvmName
|
||||||
|
|
||||||
@SchedulableFlow
|
@SchedulableFlow
|
||||||
|
@InitiatingFlow
|
||||||
class ScheduledFlow(private val stateRef: StateRef) : FlowLogic<Unit>() {
|
class ScheduledFlow(private val stateRef: StateRef) : FlowLogic<Unit>() {
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call() {
|
override fun call() {
|
||||||
@ -35,7 +33,15 @@ class ScheduledFlow(private val stateRef: StateRef) : FlowLogic<Unit>() {
|
|||||||
.addOutputState(newStateOutput, DummyContract.PROGRAM_ID)
|
.addOutputState(newStateOutput, DummyContract.PROGRAM_ID)
|
||||||
.addCommand(dummyCommand(ourIdentity.owningKey))
|
.addCommand(dummyCommand(ourIdentity.owningKey))
|
||||||
val tx = serviceHub.signInitialTransaction(builder)
|
val tx = serviceHub.signInitialTransaction(builder)
|
||||||
subFlow(FinalityFlow(tx, setOf(scheduledState.destination)))
|
subFlow(FinalityFlow(tx, initiateFlow(scheduledState.destination)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@InitiatedBy(ScheduledFlow::class)
|
||||||
|
class ScheduledResponderFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call() {
|
||||||
|
subFlow(ReceiveFinalityFlow(otherSide))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,9 +49,9 @@ class SendMessageFlow(private val message: Message, private val notary: Party, p
|
|||||||
return if (reciepent != null) {
|
return if (reciepent != null) {
|
||||||
val session = initiateFlow(reciepent)
|
val session = initiateFlow(reciepent)
|
||||||
subFlow(SendTransactionFlow(session, signedTx))
|
subFlow(SendTransactionFlow(session, signedTx))
|
||||||
subFlow(FinalityFlow(signedTx, setOf(reciepent), FINALISING_TRANSACTION.childProgressTracker()))
|
subFlow(FinalityFlow(signedTx, listOf(session), FINALISING_TRANSACTION.childProgressTracker()))
|
||||||
} else {
|
} else {
|
||||||
subFlow(FinalityFlow(signedTx, FINALISING_TRANSACTION.childProgressTracker()))
|
subFlow(FinalityFlow(signedTx, emptyList(), FINALISING_TRANSACTION.childProgressTracker()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -649,12 +649,39 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun installCoreFlows() {
|
private fun installCoreFlows() {
|
||||||
flowManager.registerInitiatedCoreFlowFactory(FinalityFlow::class, FinalityHandler::class, ::FinalityHandler)
|
installFinalityHandler()
|
||||||
flowManager.registerInitiatedCoreFlowFactory(NotaryChangeFlow::class, NotaryChangeHandler::class, ::NotaryChangeHandler)
|
flowManager.registerInitiatedCoreFlowFactory(NotaryChangeFlow::class, NotaryChangeHandler::class, ::NotaryChangeHandler)
|
||||||
flowManager.registerInitiatedCoreFlowFactory(ContractUpgradeFlow.Initiate::class, NotaryChangeHandler::class, ::ContractUpgradeHandler)
|
flowManager.registerInitiatedCoreFlowFactory(ContractUpgradeFlow.Initiate::class, NotaryChangeHandler::class, ::ContractUpgradeHandler)
|
||||||
|
// TODO Make this an inlined flow (and remove this flow mapping!), which should be possible now that FinalityFlow is also inlined
|
||||||
flowManager.registerInitiatedCoreFlowFactory(SwapIdentitiesFlow::class, SwapIdentitiesHandler::class, ::SwapIdentitiesHandler)
|
flowManager.registerInitiatedCoreFlowFactory(SwapIdentitiesFlow::class, SwapIdentitiesHandler::class, ::SwapIdentitiesHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The FinalityHandler is insecure as it blindly accepts any and all transactions into the node's local vault without doing any checks.
|
||||||
|
// To plug this hole, the sending-side FinalityFlow has been made inlined with an inlined ReceiveFinalityFlow counterpart. The old
|
||||||
|
// FinalityFlow API is gated to only work with old CorDapps (those whose target platform version < 4), and the FinalityHandler will only
|
||||||
|
// work if there is at least one old CorDapp loaded (to preserve backwards compatibility).
|
||||||
|
//
|
||||||
|
// If an attempt is made to send us a transaction via FinalityHandler, and it's disabled, we will reject the request at the session-init
|
||||||
|
// level by throwing a FinalityHandlerDisabled exception. This is picked up by the flow hospital which will not send the error back
|
||||||
|
// (immediately) and instead pause the request by keeping it un-acknowledged in the message broker. This means the request isn't lost
|
||||||
|
// across node restarts and allows the node operator time to accept or reject the request.
|
||||||
|
// TODO Add public API to allow the node operator to accept or reject
|
||||||
|
private fun installFinalityHandler() {
|
||||||
|
// Disable the insecure FinalityHandler if none of the loaded CorDapps are old enough to require it.
|
||||||
|
val cordappsNeedingFinalityHandler = cordappLoader.cordapps.filter { it.info.targetPlatformVersion < 4 }
|
||||||
|
if (cordappsNeedingFinalityHandler.isEmpty()) {
|
||||||
|
log.info("FinalityHandler is disabled as there are no CorDapps loaded which require it")
|
||||||
|
} else {
|
||||||
|
log.warn("FinalityHandler is enabled as there are CorDapps that require it: ${cordappsNeedingFinalityHandler.map { it.info }}. " +
|
||||||
|
"This is insecure and it is strongly recommended that newer versions of these CorDapps be used instead.")
|
||||||
|
}
|
||||||
|
val disabled = cordappsNeedingFinalityHandler.isEmpty()
|
||||||
|
flowManager.registerInitiatedCoreFlowFactory(FinalityFlow::class, FinalityHandler::class) {
|
||||||
|
if (disabled) throw SessionRejectException.FinalityHandlerDisabled()
|
||||||
|
FinalityHandler(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected open fun makeTransactionStorage(transactionCacheSizeBytes: Long): WritableTransactionStorage {
|
protected open fun makeTransactionStorage(transactionCacheSizeBytes: Long): WritableTransactionStorage {
|
||||||
return DBTransactionStorage(database, cacheFactory)
|
return DBTransactionStorage(database, cacheFactory)
|
||||||
}
|
}
|
||||||
|
@ -7,21 +7,15 @@ import net.corda.core.contracts.requireThat
|
|||||||
import net.corda.core.flows.*
|
import net.corda.core.flows.*
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.internal.ContractUpgradeUtils
|
import net.corda.core.internal.ContractUpgradeUtils
|
||||||
import net.corda.core.transactions.ContractUpgradeWireTransaction
|
|
||||||
import net.corda.core.node.StatesToRecord
|
import net.corda.core.node.StatesToRecord
|
||||||
|
import net.corda.core.transactions.ContractUpgradeWireTransaction
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
|
|
||||||
// TODO: We should have a whitelist of contracts we're willing to accept at all, and reject if the transaction
|
class FinalityHandler(val sender: FlowSession) : FlowLogic<Unit>() {
|
||||||
// includes us in any outside that list. Potentially just if it includes any outside that list at all.
|
|
||||||
// TODO: Do we want to be able to reject specific transactions on more complex rules, for example reject incoming
|
|
||||||
// cash without from unknown parties?
|
|
||||||
class FinalityHandler(private val sender: FlowSession) : FlowLogic<Unit>() {
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call() {
|
override fun call() {
|
||||||
subFlow(ReceiveTransactionFlow(sender, true, StatesToRecord.ONLY_RELEVANT))
|
subFlow(ReceiveTransactionFlow(sender, true, StatesToRecord.ONLY_RELEVANT))
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun sender(): Party = sender.counterparty
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class NotaryChangeHandler(otherSideSession: FlowSession) : AbstractStateReplacementFlow.Acceptor<Party>(otherSideSession) {
|
class NotaryChangeHandler(otherSideSession: FlowSession) : AbstractStateReplacementFlow.Acceptor<Party>(otherSideSession) {
|
||||||
@ -54,10 +48,13 @@ class ContractUpgradeHandler(otherSide: FlowSession) : AbstractStateReplacementF
|
|||||||
override fun verifyProposal(stx: SignedTransaction, proposal: AbstractStateReplacementFlow.Proposal<Class<out UpgradedContract<ContractState, *>>>) {
|
override fun verifyProposal(stx: SignedTransaction, proposal: AbstractStateReplacementFlow.Proposal<Class<out UpgradedContract<ContractState, *>>>) {
|
||||||
// Retrieve signed transaction from our side, we will apply the upgrade logic to the transaction on our side, and
|
// Retrieve signed transaction from our side, we will apply the upgrade logic to the transaction on our side, and
|
||||||
// verify outputs matches the proposed upgrade.
|
// verify outputs matches the proposed upgrade.
|
||||||
val ourSTX = serviceHub.validatedTransactions.getTransaction(proposal.stateRef.txhash)
|
val ourSTX = requireNotNull(serviceHub.validatedTransactions.getTransaction(proposal.stateRef.txhash)) {
|
||||||
requireNotNull(ourSTX) { "We don't have a copy of the referenced state" }
|
"We don't have a copy of the referenced state"
|
||||||
val oldStateAndRef = ourSTX!!.resolveBaseTransaction(serviceHub).outRef<ContractState>(proposal.stateRef.index)
|
}
|
||||||
val authorisedUpgrade = serviceHub.contractUpgradeService.getAuthorisedContractUpgrade(oldStateAndRef.ref) ?: throw IllegalStateException("Contract state upgrade is unauthorised. State hash : ${oldStateAndRef.ref}")
|
val oldStateAndRef = ourSTX.resolveBaseTransaction(serviceHub).outRef<ContractState>(proposal.stateRef.index)
|
||||||
|
val authorisedUpgrade = checkNotNull(serviceHub.contractUpgradeService.getAuthorisedContractUpgrade(oldStateAndRef.ref)) {
|
||||||
|
"Contract state upgrade is unauthorised. State hash : ${oldStateAndRef.ref}"
|
||||||
|
}
|
||||||
val proposedTx = stx.coreTransaction as ContractUpgradeWireTransaction
|
val proposedTx = stx.coreTransaction as ContractUpgradeWireTransaction
|
||||||
val expectedTx = ContractUpgradeUtils.assembleUpgradeTx(oldStateAndRef, proposal.modification, proposedTx.privacySalt, serviceHub)
|
val expectedTx = ContractUpgradeUtils.assembleUpgradeTx(oldStateAndRef, proposal.modification, proposedTx.privacySalt, serviceHub)
|
||||||
requireThat {
|
requireThat {
|
||||||
|
@ -12,5 +12,8 @@ open class SessionRejectException(message: String) : CordaException(message) {
|
|||||||
class NotAFlow(val initiatorClass: Class<*>) : SessionRejectException("${initiatorClass.name} is not a flow")
|
class NotAFlow(val initiatorClass: Class<*>) : SessionRejectException("${initiatorClass.name} is not a flow")
|
||||||
|
|
||||||
class NotRegistered(val initiatorFlowClass: Class<out FlowLogic<*>>) : SessionRejectException("${initiatorFlowClass.name} is not registered")
|
class NotRegistered(val initiatorFlowClass: Class<out FlowLogic<*>>) : SessionRejectException("${initiatorFlowClass.name} is not registered")
|
||||||
}
|
|
||||||
|
|
||||||
|
class FinalityHandlerDisabled : SessionRejectException("Counterparty attempting to use the old insecure API of FinalityFlow. However this " +
|
||||||
|
"API is disabled on this node since there no CorDapps installed that require it. It may be that the counterparty is running an " +
|
||||||
|
"older verison of a CorDapp.")
|
||||||
|
}
|
||||||
|
@ -42,6 +42,9 @@ class StaffedFlowHospital(private val flowMessaging: FlowMessaging, private val
|
|||||||
// installed on restart, at which point the message will be able proceed as normal. If not then it will need
|
// installed on restart, at which point the message will be able proceed as normal. If not then it will need
|
||||||
// to be dropped manually.
|
// to be dropped manually.
|
||||||
Outcome.OVERNIGHT_OBSERVATION
|
Outcome.OVERNIGHT_OBSERVATION
|
||||||
|
} else if (error is SessionRejectException.FinalityHandlerDisabled) {
|
||||||
|
// TODO We need a way to be able to give the green light to such a session-init message
|
||||||
|
Outcome.OVERNIGHT_OBSERVATION
|
||||||
} else {
|
} else {
|
||||||
Outcome.UNTREATABLE
|
Outcome.UNTREATABLE
|
||||||
}
|
}
|
||||||
@ -284,7 +287,8 @@ class StaffedFlowHospital(private val flowMessaging: FlowMessaging, private val
|
|||||||
|
|
||||||
private fun warn(flowLogic: FinalityHandler, flowFiber: FlowFiber, currentState: StateMachineState) {
|
private fun warn(flowLogic: FinalityHandler, flowFiber: FlowFiber, currentState: StateMachineState) {
|
||||||
log.warn("Flow ${flowFiber.id} failed to be finalised. Manual intervention may be required before retrying " +
|
log.warn("Flow ${flowFiber.id} failed to be finalised. Manual intervention may be required before retrying " +
|
||||||
"the flow by re-starting the node. State machine state: $currentState, initiating party was: ${flowLogic.sender().name}")
|
"the flow by re-starting the node. State machine state: $currentState, initiating party was: " +
|
||||||
|
"${flowLogic.sender.counterparty}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,9 @@ package net.corda.node.modes.draining
|
|||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
import net.corda.core.flows.FinalityFlow
|
import net.corda.core.flows.*
|
||||||
import net.corda.core.flows.FlowLogic
|
|
||||||
import net.corda.core.flows.FlowLogicRefFactory
|
|
||||||
import net.corda.core.flows.SchedulableFlow
|
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.internal.packageName
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
import net.corda.core.utilities.contextLogger
|
import net.corda.core.utilities.contextLogger
|
||||||
import net.corda.core.utilities.getOrThrow
|
import net.corda.core.utilities.getOrThrow
|
||||||
@ -43,7 +41,10 @@ class ScheduledFlowsDrainingModeTest {
|
|||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
mockNet = InternalMockNetwork(cordappsForAllNodes = cordappsForPackages("net.corda.testing.contracts"), threadPerNode = true)
|
mockNet = InternalMockNetwork(
|
||||||
|
cordappsForAllNodes = cordappsForPackages("net.corda.testing.contracts", javaClass.packageName),
|
||||||
|
threadPerNode = true
|
||||||
|
)
|
||||||
aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME))
|
aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME))
|
||||||
bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME))
|
bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME))
|
||||||
notary = mockNet.defaultNotaryIdentity
|
notary = mockNet.defaultNotaryIdentity
|
||||||
@ -112,6 +113,7 @@ class ScheduledFlowsDrainingModeTest {
|
|||||||
override val participants: List<Party> get() = listOf(source, destination)
|
override val participants: List<Party> get() = listOf(source, destination)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@InitiatingFlow
|
||||||
class InsertInitialStateFlow(private val destination: Party, private val notary: Party) : FlowLogic<Unit>() {
|
class InsertInitialStateFlow(private val destination: Party, private val notary: Party) : FlowLogic<Unit>() {
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call() {
|
override fun call() {
|
||||||
@ -120,11 +122,20 @@ class ScheduledFlowsDrainingModeTest {
|
|||||||
.addOutputState(scheduledState, DummyContract.PROGRAM_ID)
|
.addOutputState(scheduledState, DummyContract.PROGRAM_ID)
|
||||||
.addCommand(dummyCommand(ourIdentity.owningKey))
|
.addCommand(dummyCommand(ourIdentity.owningKey))
|
||||||
val tx = serviceHub.signInitialTransaction(builder)
|
val tx = serviceHub.signInitialTransaction(builder)
|
||||||
subFlow(FinalityFlow(tx))
|
subFlow(FinalityFlow(tx, initiateFlow(destination)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@InitiatedBy(InsertInitialStateFlow::class)
|
||||||
|
class InsertInitialStateResponderFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call() {
|
||||||
|
subFlow(ReceiveFinalityFlow(otherSide))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SchedulableFlow
|
@SchedulableFlow
|
||||||
|
@InitiatingFlow
|
||||||
class ScheduledFlow(private val stateRef: StateRef) : FlowLogic<Unit>() {
|
class ScheduledFlow(private val stateRef: StateRef) : FlowLogic<Unit>() {
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call() {
|
override fun call() {
|
||||||
@ -142,7 +153,15 @@ class ScheduledFlowsDrainingModeTest {
|
|||||||
.addOutputState(newStateOutput, DummyContract.PROGRAM_ID)
|
.addOutputState(newStateOutput, DummyContract.PROGRAM_ID)
|
||||||
.addCommand(dummyCommand(ourIdentity.owningKey))
|
.addCommand(dummyCommand(ourIdentity.owningKey))
|
||||||
val tx = serviceHub.signInitialTransaction(builder)
|
val tx = serviceHub.signInitialTransaction(builder)
|
||||||
subFlow(FinalityFlow(tx, setOf(scheduledState.destination)))
|
subFlow(FinalityFlow(tx, initiateFlow(scheduledState.destination)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@InitiatedBy(ScheduledFlow::class)
|
||||||
|
class ScheduledResponderFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call() {
|
||||||
|
subFlow(ReceiveFinalityFlow(otherSide))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
package net.corda.node.services
|
package net.corda.node.services
|
||||||
|
|
||||||
|
import net.corda.core.concurrent.CordaFuture
|
||||||
|
import net.corda.core.contracts.TransactionVerificationException
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.flows.FinalityFlow
|
import net.corda.core.flows.FinalityFlow
|
||||||
import net.corda.core.flows.StateMachineRunId
|
import net.corda.core.flows.StateMachineRunId
|
||||||
|
import net.corda.core.internal.cordapp.CordappInfoResolver
|
||||||
|
import net.corda.core.internal.packageName
|
||||||
import net.corda.core.toFuture
|
import net.corda.core.toFuture
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
@ -16,8 +20,10 @@ import net.corda.testing.core.BOB_NAME
|
|||||||
import net.corda.testing.core.singleIdentity
|
import net.corda.testing.core.singleIdentity
|
||||||
import net.corda.testing.node.internal.*
|
import net.corda.testing.node.internal.*
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import rx.Observable
|
||||||
|
|
||||||
class FinalityHandlerTest {
|
class FinalityHandlerTest {
|
||||||
private val mockNet = InternalMockNetwork()
|
private val mockNet = InternalMockNetwork()
|
||||||
@ -31,51 +37,115 @@ class FinalityHandlerTest {
|
|||||||
fun `sent to flow hospital on error and attempted retry on node restart`() {
|
fun `sent to flow hospital on error and attempted retry on node restart`() {
|
||||||
// Setup a network where only Alice has the finance CorDapp and it sends a cash tx to Bob who doesn't have the
|
// Setup a network where only Alice has the finance CorDapp and it sends a cash tx to Bob who doesn't have the
|
||||||
// CorDapp. Bob's FinalityHandler will error when validating the tx.
|
// CorDapp. Bob's FinalityHandler will error when validating the tx.
|
||||||
val alice = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME, additionalCordapps = setOf(FINANCE_CORDAPP)))
|
val alice = mockNet.createNode(InternalMockNodeParameters(
|
||||||
|
legalName = ALICE_NAME,
|
||||||
|
additionalCordapps = setOf(FINANCE_CORDAPP)
|
||||||
|
))
|
||||||
|
|
||||||
var bob = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME))
|
var bob = mockNet.createNode(InternalMockNodeParameters(
|
||||||
|
legalName = BOB_NAME,
|
||||||
|
// The node disables the FinalityHandler completely if there are no old CorDapps loaded, so we need to add
|
||||||
|
// a token old CorDapp to keep the handler running.
|
||||||
|
additionalCordapps = setOf(cordappForPackages(javaClass.packageName).withTargetVersion(3))
|
||||||
|
))
|
||||||
|
|
||||||
val stx = TransactionBuilder(mockNet.defaultNotaryIdentity).let {
|
val stx = alice.issueCashTo(bob)
|
||||||
Cash().generateIssue(
|
val finalityHandlerId = bob.trackFinalityHandlerId().run {
|
||||||
it,
|
alice.finaliseWithOldApi(stx)
|
||||||
1000.POUNDS.issuedBy(alice.info.singleIdentity().ref(0)),
|
getOrThrow()
|
||||||
bob.info.singleIdentity(),
|
|
||||||
mockNet.defaultNotaryIdentity
|
|
||||||
)
|
|
||||||
alice.services.signInitialTransaction(it)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val finalityHandlerIdFuture = bob.smm.track()
|
bob.assertFlowSentForObservationDueToConstraintError(finalityHandlerId)
|
||||||
.updates
|
assertThat(bob.getTransaction(stx.id)).isNull()
|
||||||
.filter { it.logic is FinalityHandler }
|
|
||||||
.map { it.logic.runId }
|
|
||||||
.toFuture()
|
|
||||||
|
|
||||||
val finalisedTx = alice.services.startFlow(FinalityFlow(stx)).run {
|
|
||||||
mockNet.runNetwork()
|
|
||||||
resultFuture.getOrThrow()
|
|
||||||
}
|
|
||||||
val finalityHandlerId = finalityHandlerIdFuture.getOrThrow()
|
|
||||||
|
|
||||||
bob.assertFlowSentForObservation(finalityHandlerId)
|
|
||||||
assertThat(bob.getTransaction(finalisedTx.id)).isNull()
|
|
||||||
|
|
||||||
bob = mockNet.restartNode(bob)
|
bob = mockNet.restartNode(bob)
|
||||||
// Since we've not done anything to fix the orignal error, we expect the finality handler to be sent to the hospital
|
// Since we've not done anything to fix the orignal error, we expect the finality handler to be sent to the hospital
|
||||||
// again on restart
|
// again on restart
|
||||||
bob.assertFlowSentForObservation(finalityHandlerId)
|
bob.assertFlowSentForObservationDueToConstraintError(finalityHandlerId)
|
||||||
assertThat(bob.getTransaction(finalisedTx.id)).isNull()
|
assertThat(bob.getTransaction(stx.id)).isNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun TestStartedNode.assertFlowSentForObservation(runId: StateMachineRunId) {
|
@Test
|
||||||
val keptInForObservation = smm.flowHospital
|
fun `disabled if there are no old CorDapps loaded`() {
|
||||||
.track()
|
val alice = mockNet.createNode(InternalMockNodeParameters(
|
||||||
.let { it.updates.startWith(it.snapshot) }
|
legalName = ALICE_NAME,
|
||||||
.ofType(MedicalRecord.Flow::class.java)
|
additionalCordapps = setOf(FINANCE_CORDAPP)
|
||||||
.filter { it.flowId == runId && it.outcome == Outcome.OVERNIGHT_OBSERVATION }
|
))
|
||||||
|
|
||||||
|
val bob = mockNet.createNode(InternalMockNodeParameters(
|
||||||
|
legalName = BOB_NAME,
|
||||||
|
// Make sure the target version is 4, and not the current platform version which may be greater
|
||||||
|
additionalCordapps = setOf(FINANCE_CORDAPP.withTargetVersion(4))
|
||||||
|
))
|
||||||
|
|
||||||
|
val stx = alice.issueCashTo(bob)
|
||||||
|
val finalityFuture = alice.finaliseWithOldApi(stx)
|
||||||
|
|
||||||
|
val record = bob.medicalRecordsOfType<MedicalRecord.SessionInit>()
|
||||||
.toBlocking()
|
.toBlocking()
|
||||||
.first()
|
.first()
|
||||||
assertThat(keptInForObservation.by).contains(FinalityDoctor)
|
assertThat(record.outcome).isEqualTo(Outcome.OVERNIGHT_OBSERVATION)
|
||||||
|
assertThat(record.sender).isEqualTo(alice.info.singleIdentity())
|
||||||
|
assertThat(record.initiatorFlowClassName).isEqualTo(FinalityFlow::class.java.name)
|
||||||
|
|
||||||
|
assertThat(bob.getTransaction(stx.id)).isNull()
|
||||||
|
|
||||||
|
// Drop the session-init so that Alice gets the error message
|
||||||
|
assertThat(finalityFuture).isNotDone()
|
||||||
|
bob.smm.flowHospital.dropSessionInit(record.id)
|
||||||
|
mockNet.runNetwork()
|
||||||
|
assertThatThrownBy {
|
||||||
|
finalityFuture.getOrThrow()
|
||||||
|
}.hasMessageContaining("Counterparty attempting to use the old insecure API of FinalityFlow")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TestStartedNode.issueCashTo(recipient: TestStartedNode): SignedTransaction {
|
||||||
|
return TransactionBuilder(mockNet.defaultNotaryIdentity).let {
|
||||||
|
Cash().generateIssue(
|
||||||
|
it,
|
||||||
|
1000.POUNDS.issuedBy(info.singleIdentity().ref(0)),
|
||||||
|
recipient.info.singleIdentity(),
|
||||||
|
mockNet.defaultNotaryIdentity
|
||||||
|
)
|
||||||
|
services.signInitialTransaction(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TestStartedNode.trackFinalityHandlerId(): CordaFuture<StateMachineRunId> {
|
||||||
|
return smm
|
||||||
|
.track()
|
||||||
|
.updates
|
||||||
|
.filter { it.logic is FinalityHandler }
|
||||||
|
.map { it.logic.runId }
|
||||||
|
.toFuture()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TestStartedNode.finaliseWithOldApi(stx: SignedTransaction): CordaFuture<SignedTransaction> {
|
||||||
|
return CordappInfoResolver.withCordappInfo(targetPlatformVersion = 3) {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
services.startFlow(FinalityFlow(stx)).resultFuture.apply {
|
||||||
|
mockNet.runNetwork()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified R : MedicalRecord> TestStartedNode.medicalRecordsOfType(): Observable<R> {
|
||||||
|
return smm
|
||||||
|
.flowHospital
|
||||||
|
.track()
|
||||||
|
.let { it.updates.startWith(it.snapshot) }
|
||||||
|
.ofType(R::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TestStartedNode.assertFlowSentForObservationDueToConstraintError(runId: StateMachineRunId) {
|
||||||
|
val observation = medicalRecordsOfType<MedicalRecord.Flow>()
|
||||||
|
.filter { it.flowId == runId }
|
||||||
|
.toBlocking()
|
||||||
|
.first()
|
||||||
|
assertThat(observation.outcome).isEqualTo(Outcome.OVERNIGHT_OBSERVATION)
|
||||||
|
assertThat(observation.by).contains(FinalityDoctor)
|
||||||
|
val error = observation.errors.single()
|
||||||
|
assertThat(error).isInstanceOf(TransactionVerificationException.ContractConstraintRejection::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun TestStartedNode.getTransaction(id: SecureHash): SignedTransaction? {
|
private fun TestStartedNode.getTransaction(id: SecureHash): SignedTransaction? {
|
||||||
|
@ -65,7 +65,7 @@ class ServiceHubConcurrentUsageTest {
|
|||||||
val issuer = ourIdentity.ref(OpaqueBytes.of(0))
|
val issuer = ourIdentity.ref(OpaqueBytes.of(0))
|
||||||
Cash().generateIssue(builder, 10.DOLLARS.issuedBy(issuer), ourIdentity, notary)
|
Cash().generateIssue(builder, 10.DOLLARS.issuedBy(issuer), ourIdentity, notary)
|
||||||
val stx = serviceHub.signInitialTransaction(builder)
|
val stx = serviceHub.signInitialTransaction(builder)
|
||||||
return subFlow(FinalityFlow(stx))
|
return subFlow(FinalityFlow(stx, emptyList()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -143,7 +143,7 @@ class TimedFlowTests {
|
|||||||
setTimeWindow(services.clock.instant(), 30.seconds)
|
setTimeWindow(services.clock.instant(), 30.seconds)
|
||||||
addOutputState(DummyContract.SingleOwnerState(owner = info.singleIdentity()), DummyContract.PROGRAM_ID, AlwaysAcceptAttachmentConstraint)
|
addOutputState(DummyContract.SingleOwnerState(owner = info.singleIdentity()), DummyContract.PROGRAM_ID, AlwaysAcceptAttachmentConstraint)
|
||||||
}
|
}
|
||||||
val flow = FinalityFlow(issueTx)
|
val flow = FinalityFlow(issueTx, emptyList())
|
||||||
val progressTracker = flow.progressTracker
|
val progressTracker = flow.progressTracker
|
||||||
assertNotEquals(ProgressTracker.DONE, progressTracker.currentStep)
|
assertNotEquals(ProgressTracker.DONE, progressTracker.currentStep)
|
||||||
val progressTrackerDone = getDoneFuture(flow.progressTracker)
|
val progressTrackerDone = getDoneFuture(flow.progressTracker)
|
||||||
|
@ -4,11 +4,9 @@ import co.paralleluniverse.fibers.Suspendable
|
|||||||
import net.corda.core.concurrent.CordaFuture
|
import net.corda.core.concurrent.CordaFuture
|
||||||
import net.corda.core.context.InvocationOrigin
|
import net.corda.core.context.InvocationOrigin
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
import net.corda.core.flows.FinalityFlow
|
import net.corda.core.flows.*
|
||||||
import net.corda.core.flows.FlowLogic
|
|
||||||
import net.corda.core.flows.FlowLogicRefFactory
|
|
||||||
import net.corda.core.flows.SchedulableFlow
|
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.internal.packageName
|
||||||
import net.corda.core.node.services.VaultService
|
import net.corda.core.node.services.VaultService
|
||||||
import net.corda.core.node.services.queryBy
|
import net.corda.core.node.services.queryBy
|
||||||
import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria
|
import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria
|
||||||
@ -60,6 +58,7 @@ class ScheduledFlowTests {
|
|||||||
override val participants: List<Party> get() = listOf(source, destination)
|
override val participants: List<Party> get() = listOf(source, destination)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@InitiatingFlow
|
||||||
class InsertInitialStateFlow(private val destination: Party, private val notary: Party) : FlowLogic<Unit>() {
|
class InsertInitialStateFlow(private val destination: Party, private val notary: Party) : FlowLogic<Unit>() {
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call() {
|
override fun call() {
|
||||||
@ -68,10 +67,19 @@ class ScheduledFlowTests {
|
|||||||
.addOutputState(scheduledState, DummyContract.PROGRAM_ID)
|
.addOutputState(scheduledState, DummyContract.PROGRAM_ID)
|
||||||
.addCommand(dummyCommand(ourIdentity.owningKey))
|
.addCommand(dummyCommand(ourIdentity.owningKey))
|
||||||
val tx = serviceHub.signInitialTransaction(builder)
|
val tx = serviceHub.signInitialTransaction(builder)
|
||||||
subFlow(FinalityFlow(tx))
|
subFlow(FinalityFlow(tx, initiateFlow(destination)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@InitiatedBy(InsertInitialStateFlow::class)
|
||||||
|
class InsertInitialStateResponderFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call() {
|
||||||
|
subFlow(ReceiveFinalityFlow(otherSide))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@InitiatingFlow
|
||||||
@SchedulableFlow
|
@SchedulableFlow
|
||||||
class ScheduledFlow(private val stateRef: StateRef) : FlowLogic<Unit>() {
|
class ScheduledFlow(private val stateRef: StateRef) : FlowLogic<Unit>() {
|
||||||
@Suspendable
|
@Suspendable
|
||||||
@ -90,13 +98,21 @@ class ScheduledFlowTests {
|
|||||||
.addOutputState(newStateOutput, DummyContract.PROGRAM_ID)
|
.addOutputState(newStateOutput, DummyContract.PROGRAM_ID)
|
||||||
.addCommand(dummyCommand(ourIdentity.owningKey))
|
.addCommand(dummyCommand(ourIdentity.owningKey))
|
||||||
val tx = serviceHub.signInitialTransaction(builder)
|
val tx = serviceHub.signInitialTransaction(builder)
|
||||||
subFlow(FinalityFlow(tx, setOf(scheduledState.destination)))
|
subFlow(FinalityFlow(tx, initiateFlow(scheduledState.destination)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@InitiatedBy(ScheduledFlow::class)
|
||||||
|
class ScheduledResponderFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call() {
|
||||||
|
subFlow(ReceiveFinalityFlow(otherSide))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
mockNet = InternalMockNetwork(cordappsForAllNodes = cordappsForPackages("net.corda.testing.contracts"), threadPerNode = true)
|
mockNet = InternalMockNetwork(cordappsForAllNodes = cordappsForPackages("net.corda.testing.contracts", javaClass.packageName), threadPerNode = true)
|
||||||
aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME))
|
aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME))
|
||||||
bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME))
|
bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME))
|
||||||
notary = mockNet.defaultNotaryIdentity
|
notary = mockNet.defaultNotaryIdentity
|
||||||
|
@ -7,14 +7,11 @@ import co.paralleluniverse.strands.concurrent.Semaphore
|
|||||||
import net.corda.client.rpc.notUsed
|
import net.corda.client.rpc.notUsed
|
||||||
import net.corda.core.concurrent.CordaFuture
|
import net.corda.core.concurrent.CordaFuture
|
||||||
import net.corda.core.contracts.ContractState
|
import net.corda.core.contracts.ContractState
|
||||||
import net.corda.core.contracts.StateAndRef
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.crypto.random63BitValue
|
import net.corda.core.crypto.random63BitValue
|
||||||
import net.corda.core.flows.*
|
import net.corda.core.flows.*
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.internal.FlowStateMachine
|
|
||||||
import net.corda.core.internal.concurrent.flatMap
|
import net.corda.core.internal.concurrent.flatMap
|
||||||
import net.corda.core.internal.concurrent.map
|
|
||||||
import net.corda.core.internal.uncheckedCast
|
|
||||||
import net.corda.core.messaging.MessageRecipients
|
import net.corda.core.messaging.MessageRecipients
|
||||||
import net.corda.core.node.services.PartyInfo
|
import net.corda.core.node.services.PartyInfo
|
||||||
import net.corda.core.node.services.queryBy
|
import net.corda.core.node.services.queryBy
|
||||||
@ -30,7 +27,10 @@ import net.corda.core.utilities.unwrap
|
|||||||
import net.corda.node.services.persistence.checkpoints
|
import net.corda.node.services.persistence.checkpoints
|
||||||
import net.corda.testing.contracts.DummyContract
|
import net.corda.testing.contracts.DummyContract
|
||||||
import net.corda.testing.contracts.DummyState
|
import net.corda.testing.contracts.DummyState
|
||||||
import net.corda.testing.core.*
|
import net.corda.testing.core.ALICE_NAME
|
||||||
|
import net.corda.testing.core.BOB_NAME
|
||||||
|
import net.corda.testing.core.dummyCommand
|
||||||
|
import net.corda.testing.core.singleIdentity
|
||||||
import net.corda.testing.internal.LogHelper
|
import net.corda.testing.internal.LogHelper
|
||||||
import net.corda.testing.node.InMemoryMessagingNetwork.MessageTransfer
|
import net.corda.testing.node.InMemoryMessagingNetwork.MessageTransfer
|
||||||
import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin
|
import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin
|
||||||
@ -66,7 +66,7 @@ class FlowFrameworkTests {
|
|||||||
@Before
|
@Before
|
||||||
fun setUpMockNet() {
|
fun setUpMockNet() {
|
||||||
mockNet = InternalMockNetwork(
|
mockNet = InternalMockNetwork(
|
||||||
cordappsForAllNodes = cordappsForPackages("net.corda.testing.contracts"),
|
cordappsForAllNodes = cordappsForPackages("net.corda.testing.contracts") + FINANCE_CORDAPP,
|
||||||
servicePeerAllocationStrategy = RoundRobin()
|
servicePeerAllocationStrategy = RoundRobin()
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -266,18 +266,20 @@ class FlowFrameworkTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `wait for transaction`() {
|
fun waitForLedgerCommit() {
|
||||||
val ptx = TransactionBuilder(notary = notaryIdentity)
|
val ptx = TransactionBuilder(notary = notaryIdentity)
|
||||||
.addOutputState(DummyState(), DummyContract.PROGRAM_ID)
|
.addOutputState(DummyState(), DummyContract.PROGRAM_ID)
|
||||||
.addCommand(dummyCommand(alice.owningKey))
|
.addCommand(dummyCommand(alice.owningKey))
|
||||||
val stx = aliceNode.services.signInitialTransaction(ptx)
|
val stx = aliceNode.services.signInitialTransaction(ptx)
|
||||||
|
|
||||||
val committerFiber = aliceNode.registerCordappFlowFactory(WaitingFlows.Waiter::class) {
|
val committerStx = aliceNode.registerCordappFlowFactory(CommitReceiverFlow::class) {
|
||||||
WaitingFlows.Committer(it)
|
CommitterFlow(it)
|
||||||
}.map { it.stateMachine }.map { uncheckedCast<FlowStateMachine<*>, FlowStateMachine<Any>>(it) }
|
}.flatMap { it.stateMachine.resultFuture }
|
||||||
val waiterStx = bobNode.services.startFlow(WaitingFlows.Waiter(stx, alice)).resultFuture
|
// The waitForLedgerCommit call has to occur on separate flow
|
||||||
|
val waiterStx = bobNode.services.startFlow(WaiterFlow(stx.id)).resultFuture
|
||||||
|
val commitReceiverStx = bobNode.services.startFlow(CommitReceiverFlow(stx, alice)).resultFuture
|
||||||
mockNet.runNetwork()
|
mockNet.runNetwork()
|
||||||
assertThat(waiterStx.getOrThrow()).isEqualTo(committerFiber.getOrThrow().resultFuture.getOrThrow())
|
assertThat(committerStx.getOrThrow()).isEqualTo(waiterStx.getOrThrow()).isEqualTo(commitReceiverStx.getOrThrow())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -287,10 +289,8 @@ class FlowFrameworkTests {
|
|||||||
.addCommand(dummyCommand())
|
.addCommand(dummyCommand())
|
||||||
val stx = aliceNode.services.signInitialTransaction(ptx)
|
val stx = aliceNode.services.signInitialTransaction(ptx)
|
||||||
|
|
||||||
aliceNode.registerCordappFlowFactory(WaitingFlows.Waiter::class) {
|
aliceNode.registerCordappFlowFactory(CommitReceiverFlow::class) { CommitterFlow(it) { throw Exception("Error") } }
|
||||||
WaitingFlows.Committer(it) { throw Exception("Error") }
|
val waiter = bobNode.services.startFlow(CommitReceiverFlow(stx, alice)).resultFuture
|
||||||
}
|
|
||||||
val waiter = bobNode.services.startFlow(WaitingFlows.Waiter(stx, alice)).resultFuture
|
|
||||||
mockNet.runNetwork()
|
mockNet.runNetwork()
|
||||||
assertThatExceptionOfType(UnexpectedFlowEndException::class.java).isThrownBy {
|
assertThatExceptionOfType(UnexpectedFlowEndException::class.java).isThrownBy {
|
||||||
waiter.getOrThrow()
|
waiter.getOrThrow()
|
||||||
@ -299,18 +299,10 @@ class FlowFrameworkTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `verify vault query service is tokenizable by force checkpointing within a flow`() {
|
fun `verify vault query service is tokenizable by force checkpointing within a flow`() {
|
||||||
val ptx = TransactionBuilder(notary = notaryIdentity)
|
aliceNode.registerCordappFlowFactory(VaultQueryFlow::class) { InitiatedSendFlow("Hello", it) }
|
||||||
.addOutputState(DummyState(), DummyContract.PROGRAM_ID)
|
val result = bobNode.services.startFlow(VaultQueryFlow(alice)).resultFuture
|
||||||
.addCommand(dummyCommand(alice.owningKey))
|
|
||||||
val stx = aliceNode.services.signInitialTransaction(ptx)
|
|
||||||
|
|
||||||
aliceNode.registerCordappFlowFactory(VaultQueryFlow::class) {
|
|
||||||
WaitingFlows.Committer(it)
|
|
||||||
}
|
|
||||||
val result = bobNode.services.startFlow(VaultQueryFlow(stx, alice)).resultFuture
|
|
||||||
|
|
||||||
mockNet.runNetwork()
|
mockNet.runNetwork()
|
||||||
assertThat(result.getOrThrow()).isEmpty()
|
result.getOrThrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -492,24 +484,27 @@ class FlowFrameworkTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private object WaitingFlows {
|
class WaiterFlow(private val txId: SecureHash) : FlowLogic<SignedTransaction>() {
|
||||||
@InitiatingFlow
|
@Suspendable
|
||||||
class Waiter(val stx: SignedTransaction, val otherParty: Party) : FlowLogic<SignedTransaction>() {
|
override fun call(): SignedTransaction = waitForLedgerCommit(txId)
|
||||||
@Suspendable
|
}
|
||||||
override fun call(): SignedTransaction {
|
|
||||||
val otherPartySession = initiateFlow(otherParty)
|
|
||||||
otherPartySession.send(stx)
|
|
||||||
return waitForLedgerCommit(stx.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Committer(val otherPartySession: FlowSession, val throwException: (() -> Exception)? = null) : FlowLogic<SignedTransaction>() {
|
@InitiatingFlow
|
||||||
@Suspendable
|
class CommitReceiverFlow(val stx: SignedTransaction, private val otherParty: Party) : FlowLogic<SignedTransaction>() {
|
||||||
override fun call(): SignedTransaction {
|
@Suspendable
|
||||||
val stx = otherPartySession.receive<SignedTransaction>().unwrap { it }
|
override fun call(): SignedTransaction {
|
||||||
if (throwException != null) throw throwException.invoke()
|
val otherPartySession = initiateFlow(otherParty)
|
||||||
return subFlow(FinalityFlow(stx, setOf(otherPartySession.counterparty)))
|
otherPartySession.send(stx)
|
||||||
}
|
return subFlow(ReceiveFinalityFlow(otherPartySession, expectedTxId = stx.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CommitterFlow(private val otherPartySession: FlowSession, private val throwException: (() -> Exception)? = null) : FlowLogic<SignedTransaction>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call(): SignedTransaction {
|
||||||
|
val stx = otherPartySession.receive<SignedTransaction>().unwrap { it }
|
||||||
|
if (throwException != null) throw throwException.invoke()
|
||||||
|
return subFlow(FinalityFlow(stx, otherPartySession))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -527,16 +522,15 @@ class FlowFrameworkTests {
|
|||||||
private class IncorrectCustomSendFlow(payload: String, otherParty: Party) : CustomInterface, SendFlow(payload, otherParty)
|
private class IncorrectCustomSendFlow(payload: String, otherParty: Party) : CustomInterface, SendFlow(payload, otherParty)
|
||||||
|
|
||||||
@InitiatingFlow
|
@InitiatingFlow
|
||||||
private class VaultQueryFlow(val stx: SignedTransaction, val otherParty: Party) : FlowLogic<List<StateAndRef<ContractState>>>() {
|
private class VaultQueryFlow(val otherParty: Party) : FlowLogic<Unit>() {
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call(): List<StateAndRef<ContractState>> {
|
override fun call() {
|
||||||
val otherPartySession = initiateFlow(otherParty)
|
val otherPartySession = initiateFlow(otherParty)
|
||||||
otherPartySession.send(stx)
|
// Hold onto reference here to force checkpoint of vaultService and thus
|
||||||
// hold onto reference here to force checkpoint of vaultService and thus
|
|
||||||
// prove it is registered as a tokenizableService in the node
|
// prove it is registered as a tokenizableService in the node
|
||||||
val vaultQuerySvc = serviceHub.vaultService
|
val vaultQuerySvc = serviceHub.vaultService
|
||||||
waitForLedgerCommit(stx.id)
|
otherPartySession.receive<Any>()
|
||||||
return vaultQuerySvc.queryBy<ContractState>().states
|
vaultQuerySvc.queryBy<ContractState>().states
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
package net.corda.node.services.vault
|
package net.corda.node.services.vault
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import net.corda.core.flows.FinalityFlow
|
import net.corda.core.flows.*
|
||||||
import net.corda.core.flows.FlowLogic
|
|
||||||
import net.corda.core.flows.InitiatingFlow
|
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.node.services.queryBy
|
import net.corda.core.node.services.queryBy
|
||||||
@ -26,7 +24,6 @@ import java.util.concurrent.ExecutionException
|
|||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
class VaultFlowTest {
|
class VaultFlowTest {
|
||||||
|
|
||||||
private lateinit var mockNetwork: MockNetwork
|
private lateinit var mockNetwork: MockNetwork
|
||||||
private lateinit var partyA: StartedMockNode
|
private lateinit var partyA: StartedMockNode
|
||||||
private lateinit var partyB: StartedMockNode
|
private lateinit var partyB: StartedMockNode
|
||||||
@ -72,17 +69,26 @@ class VaultFlowTest {
|
|||||||
partyB.services.vaultService.queryBy<DummyDealContract.State>().states.size
|
partyB.services.vaultService.queryBy<DummyDealContract.State>().states.size
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@InitiatingFlow
|
@InitiatingFlow
|
||||||
class Initiator(private val participants: List<Party>) : FlowLogic<Unit>() {
|
class Initiator(private val participants: List<Party>) : FlowLogic<Unit>() {
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call() {
|
override fun call() {
|
||||||
val stx = serviceHub.signInitialTransaction(TransactionBuilder(serviceHub.networkMapCache.notaryIdentities.first()).apply {
|
val stx = serviceHub.signInitialTransaction(TransactionBuilder(serviceHub.networkMapCache.notaryIdentities.first()).apply {
|
||||||
addOutputState(UniqueDummyLinearContract.State(participants, "Dummy linear id"), UNIQUE_DUMMY_LINEAR_CONTRACT_PROGRAM_ID)
|
addOutputState(UniqueDummyLinearContract.State(participants, "Dummy linear id"), UNIQUE_DUMMY_LINEAR_CONTRACT_PROGRAM_ID)
|
||||||
addOutputState(DummyDealContract.State(participants, "linear id"), DUMMY_DEAL_PROGRAM_ID)
|
addOutputState(DummyDealContract.State(participants, "linear id"), DUMMY_DEAL_PROGRAM_ID)
|
||||||
addCommand(DummyCommandData, listOf(ourIdentity.owningKey))
|
addCommand(DummyCommandData, listOf(ourIdentity.owningKey))
|
||||||
})
|
})
|
||||||
subFlow(FinalityFlow(stx))
|
val sessions = participants.mapNotNull { if (it != ourIdentity) initiateFlow(it) else null }
|
||||||
|
subFlow(FinalityFlow(stx, sessions))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@InitiatedBy(Initiator::class)
|
||||||
|
class Responder(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call() {
|
||||||
|
subFlow(ReceiveFinalityFlow(otherSide))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -101,12 +101,13 @@ class VaultSoftLockManagerTest {
|
|||||||
object CommandDataImpl : CommandData
|
object CommandDataImpl : CommandData
|
||||||
|
|
||||||
class ClientLogic(nodePair: NodePair, val state: ContractState) : NodePair.AbstractClientLogic<List<ContractState>>(nodePair) {
|
class ClientLogic(nodePair: NodePair, val state: ContractState) : NodePair.AbstractClientLogic<List<ContractState>>(nodePair) {
|
||||||
override fun callImpl() = run {
|
override fun callImpl(): List<ContractState> {
|
||||||
subFlow(FinalityFlow(serviceHub.signInitialTransaction(TransactionBuilder(notary = ourIdentity).apply {
|
val stx = serviceHub.signInitialTransaction(TransactionBuilder(notary = ourIdentity).apply {
|
||||||
addOutputState(state, ContractImpl::class.jvmName)
|
addOutputState(state, ContractImpl::class.jvmName)
|
||||||
addCommand(CommandDataImpl, ourIdentity.owningKey)
|
addCommand(CommandDataImpl, ourIdentity.owningKey)
|
||||||
})))
|
})
|
||||||
serviceHub.vaultService.queryBy<ContractState>(VaultQueryCriteria(softLockingCondition = SoftLockingCondition(LOCKED_ONLY))).states.map {
|
subFlow(FinalityFlow(stx, emptyList()))
|
||||||
|
return serviceHub.vaultService.queryBy<ContractState>(VaultQueryCriteria(softLockingCondition = SoftLockingCondition(LOCKED_ONLY))).states.map {
|
||||||
it.state.data
|
it.state.data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,16 +8,14 @@ import net.corda.core.contracts.Contract
|
|||||||
import net.corda.core.contracts.ContractState
|
import net.corda.core.contracts.ContractState
|
||||||
import net.corda.core.contracts.TypeOnlyCommandData
|
import net.corda.core.contracts.TypeOnlyCommandData
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.flows.FinalityFlow
|
import net.corda.core.flows.*
|
||||||
import net.corda.core.flows.FlowLogic
|
|
||||||
import net.corda.core.flows.StartableByRPC
|
|
||||||
import net.corda.core.flows.StartableByService
|
|
||||||
import net.corda.core.identity.AbstractParty
|
import net.corda.core.identity.AbstractParty
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.internal.Emoji
|
import net.corda.core.internal.Emoji
|
||||||
import net.corda.core.internal.InputStreamAndHash
|
import net.corda.core.internal.InputStreamAndHash
|
||||||
import net.corda.core.messaging.CordaRPCOps
|
import net.corda.core.messaging.CordaRPCOps
|
||||||
import net.corda.core.messaging.startTrackedFlow
|
import net.corda.core.messaging.startTrackedFlow
|
||||||
|
import net.corda.core.node.StatesToRecord
|
||||||
import net.corda.core.transactions.LedgerTransaction
|
import net.corda.core.transactions.LedgerTransaction
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
@ -106,6 +104,7 @@ private fun sender(rpc: CordaRPCOps, inputStream: InputStream, hash: SecureHash.
|
|||||||
}
|
}
|
||||||
// DOCEND 2
|
// DOCEND 2
|
||||||
|
|
||||||
|
@InitiatingFlow
|
||||||
@StartableByRPC
|
@StartableByRPC
|
||||||
class AttachmentDemoFlow(private val otherSide: Party,
|
class AttachmentDemoFlow(private val otherSide: Party,
|
||||||
private val notary: Party,
|
private val notary: Party,
|
||||||
@ -125,10 +124,19 @@ class AttachmentDemoFlow(private val otherSide: Party,
|
|||||||
|
|
||||||
progressTracker.currentStep = SIGNING
|
progressTracker.currentStep = SIGNING
|
||||||
|
|
||||||
// Send the transaction to the other recipient
|
|
||||||
val stx = serviceHub.signInitialTransaction(ptx)
|
val stx = serviceHub.signInitialTransaction(ptx)
|
||||||
|
|
||||||
return subFlow(FinalityFlow(stx, setOf(otherSide)))
|
// Send the transaction to the other recipient
|
||||||
|
return subFlow(FinalityFlow(stx, initiateFlow(otherSide)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@InitiatedBy(AttachmentDemoFlow::class)
|
||||||
|
class StoreAttachmentFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call() {
|
||||||
|
// As a non-participant to the transaction we need to record all states
|
||||||
|
subFlow(ReceiveFinalityFlow(otherSide, statesToRecord = StatesToRecord.ALL_VISIBLE))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ class TestCommsFlowInitiator(private val x500Name: CordaX500Name? = null) : Flow
|
|||||||
tx.addOutputState(CommsTestState(responses, serviceHub.myInfo.legalIdentities.first()), CommsTestContract::class.java.name)
|
tx.addOutputState(CommsTestState(responses, serviceHub.myInfo.legalIdentities.first()), CommsTestContract::class.java.name)
|
||||||
tx.addCommand(CommsTestCommand, serviceHub.myInfo.legalIdentities.first().owningKey)
|
tx.addCommand(CommsTestCommand, serviceHub.myInfo.legalIdentities.first().owningKey)
|
||||||
val signedTx = serviceHub.signInitialTransaction(tx)
|
val signedTx = serviceHub.signInitialTransaction(tx)
|
||||||
subFlow(FinalityFlow(signedTx))
|
subFlow(FinalityFlow(signedTx, emptyList()))
|
||||||
return responses
|
return responses
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,14 +32,14 @@ class TestNotaryFlow : FlowLogic<String>() {
|
|||||||
issueBuilder.addOutputState(NotaryTestState(notary.name.toString(), myIdentity), NotaryTestContract::class.java.name)
|
issueBuilder.addOutputState(NotaryTestState(notary.name.toString(), myIdentity), NotaryTestContract::class.java.name)
|
||||||
issueBuilder.addCommand(NotaryTestCommand, myIdentity.owningKey)
|
issueBuilder.addCommand(NotaryTestCommand, myIdentity.owningKey)
|
||||||
val signedTx = serviceHub.signInitialTransaction(issueBuilder)
|
val signedTx = serviceHub.signInitialTransaction(issueBuilder)
|
||||||
val issueResult = subFlow(FinalityFlow(signedTx))
|
val issueResult = subFlow(FinalityFlow(signedTx, emptyList()))
|
||||||
progressTracker.currentStep = ISSUED
|
progressTracker.currentStep = ISSUED
|
||||||
val destroyBuilder = TransactionBuilder()
|
val destroyBuilder = TransactionBuilder()
|
||||||
destroyBuilder.notary = notary
|
destroyBuilder.notary = notary
|
||||||
destroyBuilder.addInputState(issueResult.tx.outRefsOfType<NotaryTestState>().first())
|
destroyBuilder.addInputState(issueResult.tx.outRefsOfType<NotaryTestState>().first())
|
||||||
destroyBuilder.addCommand(NotaryTestCommand, myIdentity.owningKey)
|
destroyBuilder.addCommand(NotaryTestCommand, myIdentity.owningKey)
|
||||||
val signedDestroyT = serviceHub.signInitialTransaction(destroyBuilder)
|
val signedDestroyT = serviceHub.signInitialTransaction(destroyBuilder)
|
||||||
val result = subFlow(FinalityFlow(signedDestroyT))
|
val result = subFlow(FinalityFlow(signedDestroyT, emptyList()))
|
||||||
progressTracker.currentStep = DESTROYING
|
progressTracker.currentStep = DESTROYING
|
||||||
progressTracker.currentStep = FINALIZED
|
progressTracker.currentStep = FINALIZED
|
||||||
return "notarised: ${result.notary}::${result.tx.id}"
|
return "notarised: ${result.notary}::${result.tx.id}"
|
||||||
|
@ -3,9 +3,7 @@ package net.corda.traderdemo.flow
|
|||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import net.corda.core.contracts.Amount
|
import net.corda.core.contracts.Amount
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.flows.FinalityFlow
|
import net.corda.core.flows.*
|
||||||
import net.corda.core.flows.FlowLogic
|
|
||||||
import net.corda.core.flows.StartableByRPC
|
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
@ -22,6 +20,7 @@ import java.util.*
|
|||||||
* Flow for the Bank of Corda node to issue some commercial paper to the seller's node, to sell to the buyer.
|
* Flow for the Bank of Corda node to issue some commercial paper to the seller's node, to sell to the buyer.
|
||||||
*/
|
*/
|
||||||
@StartableByRPC
|
@StartableByRPC
|
||||||
|
@InitiatingFlow
|
||||||
class CommercialPaperIssueFlow(private val amount: Amount<Currency>,
|
class CommercialPaperIssueFlow(private val amount: Amount<Currency>,
|
||||||
private val issueRef: OpaqueBytes,
|
private val issueRef: OpaqueBytes,
|
||||||
private val recipient: Party,
|
private val recipient: Party,
|
||||||
@ -56,7 +55,7 @@ class CommercialPaperIssueFlow(private val amount: Amount<Currency>,
|
|||||||
// Sign it as ourselves.
|
// Sign it as ourselves.
|
||||||
val stx = serviceHub.signInitialTransaction(tx)
|
val stx = serviceHub.signInitialTransaction(tx)
|
||||||
|
|
||||||
subFlow(FinalityFlow(stx))
|
subFlow(FinalityFlow(stx, emptyList()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now make a dummy transaction that moves it to a new key, just to show that resolving dependencies works.
|
// Now make a dummy transaction that moves it to a new key, just to show that resolving dependencies works.
|
||||||
@ -65,7 +64,17 @@ class CommercialPaperIssueFlow(private val amount: Amount<Currency>,
|
|||||||
val builder = TransactionBuilder(notary)
|
val builder = TransactionBuilder(notary)
|
||||||
CommercialPaper().generateMove(builder, issuance.tx.outRef(0), recipient)
|
CommercialPaper().generateMove(builder, issuance.tx.outRef(0), recipient)
|
||||||
val stx = serviceHub.signInitialTransaction(builder)
|
val stx = serviceHub.signInitialTransaction(builder)
|
||||||
subFlow(FinalityFlow(stx))
|
val recipientSession = initiateFlow(recipient)
|
||||||
|
subFlow(FinalityFlow(stx, listOf(recipientSession)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@InitiatedBy(CommercialPaperIssueFlow::class)
|
||||||
|
class CommercialPaperIssueResponderFlow(private val otherSideSession: FlowSession) : FlowLogic<Unit>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call() {
|
||||||
|
// Record the move transaction
|
||||||
|
subFlow(ReceiveFinalityFlow(otherSideSession))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -586,7 +586,7 @@ data class DriverParameters(
|
|||||||
fun withNetworkParameters(networkParameters: NetworkParameters): DriverParameters = copy(networkParameters = networkParameters)
|
fun withNetworkParameters(networkParameters: NetworkParameters): DriverParameters = copy(networkParameters = networkParameters)
|
||||||
fun withNotaryCustomOverrides(notaryCustomOverrides: Map<String, Any?>): DriverParameters = copy(notaryCustomOverrides = notaryCustomOverrides)
|
fun withNotaryCustomOverrides(notaryCustomOverrides: Map<String, Any?>): DriverParameters = copy(notaryCustomOverrides = notaryCustomOverrides)
|
||||||
fun withInMemoryDB(inMemoryDB: Boolean): DriverParameters = copy(inMemoryDB = inMemoryDB)
|
fun withInMemoryDB(inMemoryDB: Boolean): DriverParameters = copy(inMemoryDB = inMemoryDB)
|
||||||
fun withCordappsForAllNodes(cordappsForAllNodes: Set<TestCordapp>?): DriverParameters = copy(cordappsForAllNodes = cordappsForAllNodes)
|
fun withCordappsForAllNodes(cordappsForAllNodes: Collection<TestCordapp>?): DriverParameters = copy(cordappsForAllNodes = cordappsForAllNodes)
|
||||||
|
|
||||||
fun copy(
|
fun copy(
|
||||||
isDebug: Boolean,
|
isDebug: Boolean,
|
||||||
|
@ -10,7 +10,7 @@ import net.corda.testing.node.internal.simplifyScanPackages
|
|||||||
*/
|
*/
|
||||||
@DoNotImplement
|
@DoNotImplement
|
||||||
interface TestCordapp {
|
interface TestCordapp {
|
||||||
/** Returns the name, defaults to "test-cordapp" if not specified. */
|
/** Returns the name, defaults to "test-name" if not specified. */
|
||||||
val name: String
|
val name: String
|
||||||
|
|
||||||
/** Returns the title, defaults to "test-title" if not specified. */
|
/** Returns the title, defaults to "test-title" if not specified. */
|
||||||
@ -51,17 +51,21 @@ interface TestCordapp {
|
|||||||
|
|
||||||
class Factory {
|
class Factory {
|
||||||
companion object {
|
companion object {
|
||||||
|
/**
|
||||||
|
* Create a [TestCordapp] object by scanning the given packages. The meta data on the CorDapp will be the
|
||||||
|
* default values, which can be changed with the wither methods.
|
||||||
|
*/
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun fromPackages(vararg packageNames: String): TestCordapp = fromPackages(packageNames.asList())
|
fun fromPackages(vararg packageNames: String): TestCordapp = fromPackages(packageNames.asList())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a [TestCordapp] object by scanning the given packages. The meta data on the CorDapp will be the
|
* Create a [TestCordapp] object by scanning the given packages. The meta data on the CorDapp will be the
|
||||||
* default values, which can be specified with the wither methods.
|
* default values, which can be changed with the wither methods.
|
||||||
*/
|
*/
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun fromPackages(packageNames: Collection<String>): TestCordapp {
|
fun fromPackages(packageNames: Collection<String>): TestCordapp {
|
||||||
return TestCordappImpl(
|
return TestCordappImpl(
|
||||||
name = "test-cordapp",
|
name = "test-name",
|
||||||
version = "1.0",
|
version = "1.0",
|
||||||
vendor = "test-vendor",
|
vendor = "test-vendor",
|
||||||
title = "test-title",
|
title = "test-title",
|
||||||
|
@ -253,12 +253,10 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe
|
|||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
internal open fun createNotaries(): List<TestStartedNode> {
|
internal open fun createNotaries(): List<TestStartedNode> {
|
||||||
val version = VersionInfo(networkParameters.minimumPlatformVersion, "Mock release", "Mock revision", "Mock Vendor")
|
|
||||||
return notarySpecs.map { (name, validating) ->
|
return notarySpecs.map { (name, validating) ->
|
||||||
createNode(InternalMockNodeParameters(
|
createNode(InternalMockNodeParameters(
|
||||||
legalName = name,
|
legalName = name,
|
||||||
configOverrides = MockNodeConfigOverrides(notary = MockNetNotaryConfig(validating)),
|
configOverrides = MockNodeConfigOverrides(notary = MockNetNotaryConfig(validating))
|
||||||
version = version
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user