mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
CORDA-577: FlowSession porting (#1530)
* Throw exception if a flow is initiated twice for the same Party * Chunk of porting * Need ReceiveTransactionFlow (cherry picked from commit 774383e) * Notaries compile * TwoPartyTrade * SimmFlow & StateRevisionFlow (cherry picked from commit da602b1) * TwoPArtyDealFlow regulator send * installCoreFlow * IRSTradeFlow UpdateBusinessDayFlow RatesFixFlow NodeInterestRates (cherry picked from commit 6c8d314) * Added recordTransaction parameter to ReceiveTransactionFlow * Some Tests, Flows * Fixed typo in record tx param * more things * Fix CollectSignatures * FlowFrameworkTests (cherry picked from commit 2c50bc3) * Fix TwoPartyTradeFlow * CustomVaultQuery (cherry picked from commit 48f88e8) * FlowsInJavaTest * WorkflowTransactionBuildTutorial * PersistentNetworkMapCacheTest * FlowCookBookJava (cherry picked from commit 9b48114) * Fix RatesFixFlow * Fix TwoPartyDealFlow to get signature of initiating side * Integration tests (cherry picked from commit dbcd965) * CordappSmokeTest (cherry picked from commit d19cbd6) * Inlined FinalityFlow * Updated uses of FinalityFlow * ContractUpgradeFlowTest passes * CollectSignaturesFlow refactor (cherry picked from commit 5e7b1a7) * Check that we are not the recipient of cash * Fix Simm demo * WorkflowTransactionBuildTutorialTest * Fix CashPaymentFlowTests * ScheduledFlowTests * FlowFrameworkTests * Add cordappPackagesToScan Driver param * FinalityFlowTests * Fix LoaderTestFlow * NodeMonitorModelTest * BankOfCordaRPCClientTest * rename to extraCordappPackagesToScan * Fixed broken merge * BankOfCordaHttpAPITest * Fix CollectSignaturesFlow * Fix annotation on DummyFlow to stop warning * Fix TraderDemoTest * Review feedback * Doc improvements and minor changes * Address some PR comments * Looping regulators into the FinalityFlow broadcast rather than sending separately in TwoPartyDealFlow. * Add Uninitiated FlowState * Add test for double initiateFlow exception * Some more s&r victims * FlowSession utilities (#1562) * Merge fix * CollectSignatureFlow can handle several signing keys * Actually handle several signing keys * update kdoc * Correct SignTransactionFlow error message * Create deprecated flows package * Add internal deprecated flows * Reverted FinalityFlow to auto-broadcast all tx participants * Move the deprecated packages into another PR
This commit is contained in:
parent
78500205df
commit
33421bdd44
@ -52,7 +52,7 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
||||
lateinit var networkMapUpdates: Observable<NetworkMapCache.MapChange>
|
||||
lateinit var newNode: (CordaX500Name) -> NodeInfo
|
||||
|
||||
override fun setup() = driver {
|
||||
override fun setup() = driver(extraCordappPackagesToScan = listOf("net.corda.finance")) {
|
||||
val cashUser = User("user1", "test", permissions = setOf(
|
||||
startFlowPermission<CashIssueFlow>(),
|
||||
startFlowPermission<CashPaymentFlow>(),
|
||||
|
@ -3,8 +3,8 @@ package net.corda.confidential
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
@ -22,10 +22,10 @@ object IdentitySyncFlow {
|
||||
* @return a mapping of well known identities to the confidential identities used in the transaction.
|
||||
*/
|
||||
// TODO: Can this be triggered automatically from [SendTransactionFlow]
|
||||
class Send(val otherSides: Set<Party>,
|
||||
class Send(val otherSideSessions: Set<FlowSession>,
|
||||
val tx: WireTransaction,
|
||||
override val progressTracker: ProgressTracker) : FlowLogic<Unit>() {
|
||||
constructor(otherSide: Party, tx: WireTransaction) : this(setOf(otherSide), tx, tracker())
|
||||
constructor(otherSide: FlowSession, tx: WireTransaction) : this(setOf(otherSide), tx, tracker())
|
||||
|
||||
companion object {
|
||||
object SYNCING_IDENTITIES : ProgressTracker.Step("Syncing identities")
|
||||
@ -45,9 +45,9 @@ object IdentitySyncFlow {
|
||||
val identityCertificates: Map<AbstractParty, PartyAndCertificate?> = identities
|
||||
.map { Pair(it, serviceHub.identityService.certificateFromKey(it.owningKey)) }.toMap()
|
||||
|
||||
otherSides.forEach { otherSide ->
|
||||
val requestedIdentities: List<AbstractParty> = sendAndReceive<List<AbstractParty>>(otherSide, confidentialIdentities).unwrap { req ->
|
||||
require(req.all { it in identityCertificates.keys }) { "${otherSide} requested a confidential identity not part of transaction: ${tx.id}" }
|
||||
otherSideSessions.forEach { otherSideSession ->
|
||||
val requestedIdentities: List<AbstractParty> = otherSideSession.sendAndReceive<List<AbstractParty>>(confidentialIdentities).unwrap { req ->
|
||||
require(req.all { it in identityCertificates.keys }) { "${otherSideSession.counterparty} requested a confidential identity not part of transaction: ${tx.id}" }
|
||||
req
|
||||
}
|
||||
val sendIdentities: List<PartyAndCertificate?> = requestedIdentities.map {
|
||||
@ -57,7 +57,7 @@ object IdentitySyncFlow {
|
||||
else
|
||||
throw IllegalStateException("Counterparty requested a confidential identity for which we do not have the certificate path: ${tx.id}")
|
||||
}
|
||||
send(otherSide, sendIdentities)
|
||||
otherSideSession.send(sendIdentities)
|
||||
}
|
||||
}
|
||||
|
||||
@ -67,7 +67,7 @@ object IdentitySyncFlow {
|
||||
* Handle an offer to provide proof of identity (in the form of certificate paths) for confidential identities which
|
||||
* we do not yet know about.
|
||||
*/
|
||||
class Receive(val otherSide: Party) : FlowLogic<Unit>() {
|
||||
class Receive(val otherSideSession: FlowSession) : FlowLogic<Unit>() {
|
||||
companion object {
|
||||
object RECEIVING_IDENTITIES : ProgressTracker.Step("Receiving confidential identities")
|
||||
object RECEIVING_CERTIFICATES : ProgressTracker.Step("Receiving certificates for unknown identities")
|
||||
@ -78,10 +78,10 @@ object IdentitySyncFlow {
|
||||
@Suspendable
|
||||
override fun call(): Unit {
|
||||
progressTracker.currentStep = RECEIVING_IDENTITIES
|
||||
val allIdentities = receive<List<AbstractParty>>(otherSide).unwrap { it }
|
||||
val allIdentities = otherSideSession.receive<List<AbstractParty>>().unwrap { it }
|
||||
val unknownIdentities = allIdentities.filter { serviceHub.identityService.partyFromAnonymous(it) == null }
|
||||
progressTracker.currentStep = RECEIVING_CERTIFICATES
|
||||
val missingIdentities = sendAndReceive<List<PartyAndCertificate>>(otherSide, unknownIdentities)
|
||||
val missingIdentities = otherSideSession.sendAndReceive<List<PartyAndCertificate>>(unknownIdentities)
|
||||
|
||||
// Batch verify the identities we've received, so we know they're all correct before we start storing them in
|
||||
// the identity service
|
||||
|
@ -18,10 +18,10 @@ import net.corda.core.utilities.unwrap
|
||||
*/
|
||||
@StartableByRPC
|
||||
@InitiatingFlow
|
||||
class SwapIdentitiesFlow(private val otherSide: Party,
|
||||
class SwapIdentitiesFlow(private val otherParty: Party,
|
||||
private val revocationEnabled: Boolean,
|
||||
override val progressTracker: ProgressTracker) : FlowLogic<LinkedHashMap<Party, AnonymousParty>>() {
|
||||
constructor(otherSide: Party) : this(otherSide, false, tracker())
|
||||
constructor(otherParty: Party) : this(otherParty, false, tracker())
|
||||
|
||||
companion object {
|
||||
object AWAITING_KEY : ProgressTracker.Step("Awaiting key")
|
||||
@ -43,14 +43,15 @@ class SwapIdentitiesFlow(private val otherSide: Party,
|
||||
|
||||
// Special case that if we're both parties, a single identity is generated
|
||||
val identities = LinkedHashMap<Party, AnonymousParty>()
|
||||
if (serviceHub.myInfo.isLegalIdentity(otherSide)) {
|
||||
identities.put(otherSide, legalIdentityAnonymous.party.anonymise())
|
||||
if (serviceHub.myInfo.isLegalIdentity(otherParty)) {
|
||||
identities.put(otherParty, legalIdentityAnonymous.party.anonymise())
|
||||
} else {
|
||||
val anonymousOtherSide = sendAndReceive<PartyAndCertificate>(otherSide, legalIdentityAnonymous).unwrap { confidentialIdentity ->
|
||||
validateAndRegisterIdentity(serviceHub.identityService, otherSide, confidentialIdentity)
|
||||
val otherSession = initiateFlow(otherParty)
|
||||
val anonymousOtherSide = otherSession.sendAndReceive<PartyAndCertificate>(legalIdentityAnonymous).unwrap { confidentialIdentity ->
|
||||
validateAndRegisterIdentity(serviceHub.identityService, otherSession.counterparty, confidentialIdentity)
|
||||
}
|
||||
identities.put(ourIdentity, legalIdentityAnonymous.party.anonymise())
|
||||
identities.put(otherSide, anonymousOtherSide.party.anonymise())
|
||||
identities.put(otherSession.counterparty, anonymousOtherSide.party.anonymise())
|
||||
}
|
||||
return identities
|
||||
}
|
||||
|
@ -2,13 +2,14 @@ package net.corda.confidential
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.unwrap
|
||||
|
||||
class SwapIdentitiesHandler(val otherSide: Party, val revocationEnabled: Boolean) : FlowLogic<Unit>() {
|
||||
constructor(otherSide: Party) : this(otherSide, false)
|
||||
class SwapIdentitiesHandler(val otherSideSession: FlowSession, val revocationEnabled: Boolean) : FlowLogic<Unit>() {
|
||||
constructor(otherSideSession: FlowSession) : this(otherSideSession, false)
|
||||
|
||||
companion object {
|
||||
object SENDING_KEY : ProgressTracker.Step("Sending key")
|
||||
}
|
||||
@ -20,8 +21,8 @@ class SwapIdentitiesHandler(val otherSide: Party, val revocationEnabled: Boolean
|
||||
val revocationEnabled = false
|
||||
progressTracker.currentStep = SENDING_KEY
|
||||
val legalIdentityAnonymous = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, revocationEnabled)
|
||||
sendAndReceive<PartyAndCertificate>(otherSide, legalIdentityAnonymous).unwrap { confidentialIdentity ->
|
||||
SwapIdentitiesFlow.validateAndRegisterIdentity(serviceHub.identityService, otherSide, confidentialIdentity)
|
||||
otherSideSession.sendAndReceive<PartyAndCertificate>(legalIdentityAnonymous).unwrap { confidentialIdentity ->
|
||||
SwapIdentitiesFlow.validateAndRegisterIdentity(serviceHub.identityService, otherSideSession.counterparty, confidentialIdentity)
|
||||
}
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ package net.corda.confidential
|
||||
|
||||
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.InitiatingFlow
|
||||
import net.corda.core.identity.Party
|
||||
@ -12,11 +13,7 @@ import net.corda.core.utilities.unwrap
|
||||
import net.corda.finance.DOLLARS
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.finance.flows.CashIssueAndPaymentFlow
|
||||
import net.corda.testing.ALICE
|
||||
import net.corda.testing.BOB
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
import net.corda.testing.chooseIdentity
|
||||
import net.corda.testing.getDefaultNotary
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
@ -75,19 +72,20 @@ class IdentitySyncFlowTests {
|
||||
class Initiator(val otherSide: Party, val tx: WireTransaction): FlowLogic<Boolean>() {
|
||||
@Suspendable
|
||||
override fun call(): Boolean {
|
||||
subFlow(IdentitySyncFlow.Send(otherSide, tx))
|
||||
val session = initiateFlow(otherSide)
|
||||
subFlow(IdentitySyncFlow.Send(session, tx))
|
||||
// Wait for the counterparty to indicate they're done
|
||||
return receive<Boolean>(otherSide).unwrap { it }
|
||||
return session.receive<Boolean>().unwrap { it }
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(IdentitySyncFlowTests.Initiator::class)
|
||||
class Receive(val otherSide: Party): FlowLogic<Unit>() {
|
||||
class Receive(val otherSideSession: FlowSession): FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
subFlow(IdentitySyncFlow.Receive(otherSide))
|
||||
subFlow(IdentitySyncFlow.Receive(otherSideSession))
|
||||
// Notify the initiator that we've finished syncing
|
||||
send(otherSide, true)
|
||||
otherSideSession.send(true)
|
||||
}
|
||||
}
|
||||
}
|
@ -6,13 +6,12 @@ import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.crypto.isFulfilledBy
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.UntrustworthyData
|
||||
import net.corda.core.utilities.unwrap
|
||||
import java.security.PublicKey
|
||||
|
||||
/**
|
||||
* Abstract flow to be used for replacing one state with another, for example when changing the notary of a state.
|
||||
@ -33,10 +32,8 @@ abstract class AbstractStateReplacementFlow {
|
||||
* The assembled transaction for upgrading a contract.
|
||||
*
|
||||
* @param stx signed transaction to do the upgrade.
|
||||
* @param participants the parties involved in the upgrade transaction.
|
||||
* @param myKey key
|
||||
*/
|
||||
data class UpgradeTx(val stx: SignedTransaction, val participants: Iterable<PublicKey>, val myKey: PublicKey)
|
||||
data class UpgradeTx(val stx: SignedTransaction)
|
||||
|
||||
/**
|
||||
* The [Instigator] assembles the transaction for state replacement and sends out change proposals to all participants
|
||||
@ -62,15 +59,11 @@ abstract class AbstractStateReplacementFlow {
|
||||
@Suspendable
|
||||
@Throws(StateReplacementException::class)
|
||||
override fun call(): StateAndRef<T> {
|
||||
val (stx, participantKeys, myKey) = assembleTx()
|
||||
|
||||
val (stx) = assembleTx()
|
||||
val participantSessions = getParticipantSessions()
|
||||
progressTracker.currentStep = SIGNING
|
||||
|
||||
val signatures = if (participantKeys.singleOrNull() == myKey) {
|
||||
getNotarySignatures(stx)
|
||||
} else {
|
||||
collectSignatures(participantKeys - myKey, stx)
|
||||
}
|
||||
val signatures = collectSignatures(participantSessions, stx)
|
||||
|
||||
val finalTx = stx + signatures
|
||||
serviceHub.recordTransactions(finalTx)
|
||||
@ -89,35 +82,38 @@ abstract class AbstractStateReplacementFlow {
|
||||
/**
|
||||
* Build the upgrade transaction.
|
||||
*
|
||||
* @return a triple of the transaction, the public keys of all participants, and the participating public key of
|
||||
* this node.
|
||||
* @return the transaction
|
||||
*/
|
||||
abstract protected fun assembleTx(): UpgradeTx
|
||||
|
||||
@Suspendable
|
||||
private fun collectSignatures(participants: Iterable<PublicKey>, stx: SignedTransaction): List<TransactionSignature> {
|
||||
// In identity service we record all identities we know about from network map.
|
||||
val parties = participants.map {
|
||||
serviceHub.identityService.partyFromKey(it) ?:
|
||||
throw IllegalStateException("Participant $it to state $originalState not found on the network")
|
||||
}
|
||||
/**
|
||||
* Initiate sessions with parties we want signatures from.
|
||||
*/
|
||||
open fun getParticipantSessions(): List<Pair<FlowSession, List<AbstractParty>>> {
|
||||
return serviceHub.excludeMe(serviceHub.groupAbstractPartyByWellKnownParty(originalState.state.data.participants)).map { initiateFlow(it.key) to it.value }
|
||||
}
|
||||
|
||||
val participantSignatures = parties.map { getParticipantSignature(it, stx) }
|
||||
@Suspendable
|
||||
private fun collectSignatures(sessions: List<Pair<FlowSession, List<AbstractParty>>>, stx: SignedTransaction): List<TransactionSignature> {
|
||||
val participantSignatures = sessions.map { getParticipantSignature(it.first, it.second, stx) }
|
||||
|
||||
val allPartySignedTx = stx + participantSignatures
|
||||
|
||||
val allSignatures = participantSignatures + getNotarySignatures(allPartySignedTx)
|
||||
parties.forEach { send(it, allSignatures) }
|
||||
sessions.forEach { it.first.send(allSignatures) }
|
||||
|
||||
return allSignatures
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun getParticipantSignature(party: Party, stx: SignedTransaction): TransactionSignature {
|
||||
private fun getParticipantSignature(session: FlowSession, party: List<AbstractParty>, stx: SignedTransaction): TransactionSignature {
|
||||
require(party.size == 1) {
|
||||
"We do not currently support multiple signatures from the same party ${session.counterparty}: $party"
|
||||
}
|
||||
val proposal = Proposal(originalState.ref, modification)
|
||||
subFlow(SendTransactionFlow(party, stx))
|
||||
return sendAndReceive<TransactionSignature>(party, proposal).unwrap {
|
||||
check(party.owningKey.isFulfilledBy(it.by)) { "Not signed by the required participant" }
|
||||
subFlow(SendTransactionFlow(session, stx))
|
||||
return session.sendAndReceive<TransactionSignature>(proposal).unwrap {
|
||||
check(party.single().owningKey.isFulfilledBy(it.by)) { "Not signed by the required participant" }
|
||||
it.verify(stx.id)
|
||||
it
|
||||
}
|
||||
@ -136,9 +132,9 @@ abstract class AbstractStateReplacementFlow {
|
||||
|
||||
// Type parameter should ideally be Unit but that prevents Java code from subclassing it (https://youtrack.jetbrains.com/issue/KT-15964).
|
||||
// We use Void? instead of Unit? as that's what you'd use in Java.
|
||||
abstract class Acceptor<in T>(val otherSide: Party,
|
||||
abstract class Acceptor<in T>(val initiatingSession: FlowSession,
|
||||
override val progressTracker: ProgressTracker = Acceptor.tracker()) : FlowLogic<Void?>() {
|
||||
constructor(otherSide: Party) : this(otherSide, Acceptor.tracker())
|
||||
constructor(initiatingSession: FlowSession) : this(initiatingSession, Acceptor.tracker())
|
||||
companion object {
|
||||
object VERIFYING : ProgressTracker.Step("Verifying state replacement proposal")
|
||||
object APPROVING : ProgressTracker.Step("State replacement approved")
|
||||
@ -151,9 +147,9 @@ abstract class AbstractStateReplacementFlow {
|
||||
override fun call(): Void? {
|
||||
progressTracker.currentStep = VERIFYING
|
||||
// We expect stx to have insufficient signatures here
|
||||
val stx = subFlow(ReceiveTransactionFlow(otherSide, checkSufficientSignatures = false))
|
||||
val stx = subFlow(ReceiveTransactionFlow(initiatingSession, checkSufficientSignatures = false))
|
||||
checkMySignatureRequired(stx)
|
||||
val maybeProposal: UntrustworthyData<Proposal<T>> = receive(otherSide)
|
||||
val maybeProposal: UntrustworthyData<Proposal<T>> = initiatingSession.receive()
|
||||
maybeProposal.unwrap {
|
||||
verifyProposal(stx, it)
|
||||
}
|
||||
@ -166,7 +162,7 @@ abstract class AbstractStateReplacementFlow {
|
||||
progressTracker.currentStep = APPROVING
|
||||
|
||||
val mySignature = sign(stx)
|
||||
val swapSignatures = sendAndReceive<List<TransactionSignature>>(otherSide, mySignature)
|
||||
val swapSignatures = initiatingSession.sendAndReceive<List<TransactionSignature>>(mySignature)
|
||||
|
||||
// TODO: This step should not be necessary, as signatures are re-checked in verifyRequiredSignatures.
|
||||
val allSignatures = swapSignatures.unwrap { signatures ->
|
||||
|
@ -1,28 +0,0 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.NonEmptySet
|
||||
|
||||
/**
|
||||
* Notify the specified parties about a transaction. The remote peers will download this transaction and its
|
||||
* dependency graph, verifying them all. The flow returns when all peers have acknowledged the transactions
|
||||
* as valid. Normally you wouldn't use this directly, it would be called via [FinalityFlow].
|
||||
*
|
||||
* @param notarisedTransaction transaction which has been notarised (if needed) and is ready to notify nodes about.
|
||||
* @param participants a list of participants involved in the transaction.
|
||||
* @return a list of participants who were successfully notified of the transaction.
|
||||
*/
|
||||
@InitiatingFlow
|
||||
class BroadcastTransactionFlow(val notarisedTransaction: SignedTransaction,
|
||||
val participants: NonEmptySet<Party>) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
// TODO: Messaging layer should handle this broadcast for us
|
||||
participants.filter { !serviceHub.myInfo.isLegalIdentity(it) }.forEach { participant ->
|
||||
// SendTransactionFlow allows otherParty to access our data to resolve the transaction.
|
||||
subFlow(SendTransactionFlow(participant, notarisedTransaction))
|
||||
}
|
||||
}
|
||||
}
|
@ -3,8 +3,6 @@ package net.corda.core.flows
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.crypto.isFulfilledBy
|
||||
import net.corda.core.utilities.toBase58String
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
@ -57,21 +55,25 @@ import java.security.PublicKey
|
||||
* val stx = subFlow(CollectSignaturesFlow(ptx))
|
||||
*
|
||||
* @param partiallySignedTx Transaction to collect the remaining signatures for
|
||||
* @param sessionsToCollectFrom A session for every party we need to collect a signature from. Must be an exact match.
|
||||
* @param myOptionalKeys set of keys in the transaction which are owned by this node. This includes keys used on commands, not
|
||||
* just in the states. If null, the default well known identity of the node is used.
|
||||
*/
|
||||
// TODO: AbstractStateReplacementFlow needs updating to use this flow.
|
||||
class CollectSignaturesFlow @JvmOverloads constructor (val partiallySignedTx: SignedTransaction,
|
||||
val sessionsToCollectFrom: Collection<FlowSession>,
|
||||
val myOptionalKeys: Iterable<PublicKey>?,
|
||||
override val progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : FlowLogic<SignedTransaction>() {
|
||||
@JvmOverloads constructor(partiallySignedTx: SignedTransaction, progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : this(partiallySignedTx, null, progressTracker)
|
||||
@JvmOverloads constructor(partiallySignedTx: SignedTransaction, sessionsToCollectFrom: Collection<FlowSession>, progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : this(partiallySignedTx, sessionsToCollectFrom, null, progressTracker)
|
||||
companion object {
|
||||
object COLLECTING : ProgressTracker.Step("Collecting signatures from counter-parties.")
|
||||
object VERIFYING : ProgressTracker.Step("Verifying collected signatures.")
|
||||
|
||||
@JvmStatic
|
||||
fun tracker() = ProgressTracker(COLLECTING, VERIFYING)
|
||||
|
||||
// TODO: Make the progress tracker adapt to the number of counter-parties to collect from.
|
||||
|
||||
}
|
||||
|
||||
@Suspendable override fun call(): SignedTransaction {
|
||||
@ -100,8 +102,15 @@ class CollectSignaturesFlow @JvmOverloads constructor (val partiallySignedTx: Si
|
||||
// If the unsigned counter-parties list is empty then we don't need to collect any more signatures here.
|
||||
if (unsigned.isEmpty()) return partiallySignedTx
|
||||
|
||||
val partyToKeysMap = serviceHub.groupPublicKeysByWellKnownParty(unsigned)
|
||||
// Check that we have a session for all parties. No more, no less.
|
||||
require(sessionsToCollectFrom.map { it.counterparty }.toSet() == partyToKeysMap.keys) {
|
||||
"The Initiator of CollectSignaturesFlow must pass in exactly the sessions required to sign the transaction."
|
||||
}
|
||||
// Collect signatures from all counter-parties and append them to the partially signed transaction.
|
||||
val counterpartySignatures = keysToParties(unsigned).map { collectSignature(it.first, it.second) }
|
||||
val counterpartySignatures = sessionsToCollectFrom.flatMap { session ->
|
||||
subFlow(CollectSignatureFlow(partiallySignedTx, session, partyToKeysMap[session.counterparty]!!))
|
||||
}
|
||||
val stx = partiallySignedTx + counterpartySignatures
|
||||
|
||||
// Verify all but the notary's signature if the transaction requires a notary, otherwise verify all signatures.
|
||||
@ -110,40 +119,38 @@ class CollectSignaturesFlow @JvmOverloads constructor (val partiallySignedTx: Si
|
||||
|
||||
return stx
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup the [Party] object for each [PublicKey] using the [ServiceHub.identityService].
|
||||
*
|
||||
* @return a pair of the well known identity to contact for a signature, and the public key that party should sign
|
||||
* with (this may belong to a confidential identity).
|
||||
*/
|
||||
@Suspendable private fun keysToParties(keys: Collection<PublicKey>): List<Pair<Party, PublicKey>> = keys.map {
|
||||
val party = serviceHub.identityService.partyFromAnonymous(AnonymousParty(it))
|
||||
?: throw IllegalStateException("Party ${it.toBase58String()} not found on the network.")
|
||||
Pair(party, it)
|
||||
}
|
||||
|
||||
// DOCSTART 1
|
||||
/**
|
||||
* Get and check the required signature.
|
||||
*
|
||||
* @param counterparty the party to request a signature from.
|
||||
* @param signingKey the key the party should use to sign the transaction.
|
||||
*/
|
||||
@Suspendable private fun collectSignature(counterparty: Party, signingKey: PublicKey): TransactionSignature {
|
||||
// DOCSTART 1
|
||||
/**
|
||||
* Get and check the required signature.
|
||||
*
|
||||
* @param partiallySignedTx the transaction to sign.
|
||||
* @param session the [FlowSession] to connect to to get the signature.
|
||||
* @param signingKeys the list of keys the party should use to sign the transaction.
|
||||
*/
|
||||
@Suspendable
|
||||
class CollectSignatureFlow(val partiallySignedTx: SignedTransaction, val session: FlowSession, val signingKeys: List<PublicKey>) : FlowLogic<List<TransactionSignature>>() {
|
||||
constructor(partiallySignedTx: SignedTransaction, session: FlowSession, vararg signingKeys: PublicKey) :
|
||||
this(partiallySignedTx, session, listOf(*signingKeys))
|
||||
@Suspendable
|
||||
override fun call(): List<TransactionSignature> {
|
||||
// SendTransactionFlow allows counterparty to access our data to resolve the transaction.
|
||||
subFlow(SendTransactionFlow(counterparty, partiallySignedTx))
|
||||
subFlow(SendTransactionFlow(session, partiallySignedTx))
|
||||
// Send the key we expect the counterparty to sign with - this is important where they may have several
|
||||
// keys to sign with, as it makes it faster for them to identify the key to sign with, and more straight forward
|
||||
// for us to check we have the expected signature returned.
|
||||
send(counterparty, signingKey)
|
||||
return receive<TransactionSignature>(counterparty).unwrap {
|
||||
require(signingKey.isFulfilledBy(it.by)) { "Not signed by the required signing key." }
|
||||
it
|
||||
session.send(signingKeys)
|
||||
return session.receive<List<TransactionSignature>>().unwrap { signatures ->
|
||||
require(signatures.size == signingKeys.size) { "Need signature for each signing key" }
|
||||
signatures.forEachIndexed { index, signature ->
|
||||
require(signingKeys[index].isFulfilledBy(signature.by)) { "Not signed by the required signing key." }
|
||||
}
|
||||
signatures
|
||||
}
|
||||
}
|
||||
// DOCEND 1
|
||||
}
|
||||
// DOCEND 1
|
||||
|
||||
/**
|
||||
* The [SignTransactionFlow] should be called in response to the [CollectSignaturesFlow]. It automates the signing of
|
||||
@ -159,15 +166,15 @@ class CollectSignaturesFlow @JvmOverloads constructor (val partiallySignedTx: Si
|
||||
* - Subclass [SignTransactionFlow] - this can be done inside an existing flow (as shown below)
|
||||
* - Override the [checkTransaction] method to add some custom verification logic
|
||||
* - Call the flow via [FlowLogic.subFlow]
|
||||
* - The flow returns the fully signed transaction once it has been committed to the ledger
|
||||
* - The flow returns the transaction signed with the additional signature.
|
||||
*
|
||||
* Example - checking and signing a transaction involving a [net.corda.core.contracts.DummyContract], see
|
||||
* CollectSignaturesFlowTests.kt for further examples:
|
||||
*
|
||||
* class Responder(val otherParty: Party): FlowLogic<SignedTransaction>() {
|
||||
* class Responder(val otherPartySession: FlowSession): FlowLogic<SignedTransaction>() {
|
||||
* @Suspendable override fun call(): SignedTransaction {
|
||||
* // [SignTransactionFlow] sub-classed as a singleton object.
|
||||
* val flow = object : SignTransactionFlow(otherParty) {
|
||||
* val flow = object : SignTransactionFlow(otherPartySession) {
|
||||
* @Suspendable override fun checkTransaction(stx: SignedTransaction) = requireThat {
|
||||
* val tx = stx.tx
|
||||
* val magicNumberState = tx.outputs.single().data as DummyContract.MultiOwnerState
|
||||
@ -182,9 +189,9 @@ class CollectSignaturesFlow @JvmOverloads constructor (val partiallySignedTx: Si
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @param otherParty The counter-party which is providing you a transaction to sign.
|
||||
* @param otherSideSession The session which is providing you a transaction to sign.
|
||||
*/
|
||||
abstract class SignTransactionFlow(val otherParty: Party,
|
||||
abstract class SignTransactionFlow(val otherSideSession: FlowSession,
|
||||
override val progressTracker: ProgressTracker = SignTransactionFlow.tracker()) : FlowLogic<SignedTransaction>() {
|
||||
|
||||
companion object {
|
||||
@ -192,23 +199,24 @@ abstract class SignTransactionFlow(val otherParty: Party,
|
||||
object VERIFYING : ProgressTracker.Step("Verifying transaction proposal.")
|
||||
object SIGNING : ProgressTracker.Step("Signing transaction proposal.")
|
||||
|
||||
@JvmStatic
|
||||
fun tracker() = ProgressTracker(RECEIVING, VERIFYING, SIGNING)
|
||||
}
|
||||
|
||||
@Suspendable override fun call(): SignedTransaction {
|
||||
progressTracker.currentStep = RECEIVING
|
||||
// Receive transaction and resolve dependencies, check sufficient signatures is disabled as we don't have all signatures.
|
||||
val stx = subFlow(ReceiveTransactionFlow(otherParty, checkSufficientSignatures = false))
|
||||
val stx = subFlow(ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = false))
|
||||
// Receive the signing key that the party requesting the signature expects us to sign with. Having this provided
|
||||
// means we only have to check we own that one key, rather than matching all keys in the transaction against all
|
||||
// keys we own.
|
||||
val signingKey = receive<PublicKey>(otherParty).unwrap {
|
||||
val signingKeys = otherSideSession.receive<List<PublicKey>>().unwrap { keys ->
|
||||
// TODO: We should have a faster way of verifying we own a single key
|
||||
serviceHub.keyManagementService.filterMyKeys(listOf(it)).single()
|
||||
serviceHub.keyManagementService.filterMyKeys(keys)
|
||||
}
|
||||
progressTracker.currentStep = VERIFYING
|
||||
// Check that the Responder actually needs to sign.
|
||||
checkMySignatureRequired(stx, signingKey)
|
||||
checkMySignaturesRequired(stx, signingKeys)
|
||||
// Check the signatures which have already been provided. Usually the Initiators and possibly an Oracle's.
|
||||
checkSignatures(stx)
|
||||
stx.tx.toLedgerTransaction(serviceHub).verify()
|
||||
@ -223,18 +231,19 @@ abstract class SignTransactionFlow(val otherParty: Party,
|
||||
}
|
||||
// Sign and send back our signature to the Initiator.
|
||||
progressTracker.currentStep = SIGNING
|
||||
val mySignature = serviceHub.createSignature(stx, signingKey)
|
||||
send(otherParty, mySignature)
|
||||
val mySignatures = signingKeys.map { key ->
|
||||
serviceHub.createSignature(stx, key)
|
||||
}
|
||||
otherSideSession.send(mySignatures)
|
||||
|
||||
// Return the fully signed transaction once it has been committed.
|
||||
return waitForLedgerCommit(stx.id)
|
||||
// Return the additionally signed transaction.
|
||||
return stx + mySignatures
|
||||
}
|
||||
|
||||
@Suspendable private fun checkSignatures(stx: SignedTransaction) {
|
||||
val signingIdentities = stx.sigs.map(TransactionSignature::by).mapNotNull(serviceHub.identityService::partyFromKey)
|
||||
val signingWellKnownIdentities = signingIdentities.mapNotNull(serviceHub.identityService::partyFromAnonymous)
|
||||
require(otherParty in signingWellKnownIdentities) {
|
||||
"The Initiator of CollectSignaturesFlow must have signed the transaction. Found ${signingWellKnownIdentities}, expected ${otherParty}"
|
||||
val signingWellKnownIdentities = serviceHub.groupPublicKeysByWellKnownParty(stx.sigs.map(TransactionSignature::by))
|
||||
require(otherSideSession.counterparty in signingWellKnownIdentities) {
|
||||
"The Initiator of CollectSignaturesFlow must have signed the transaction. Found ${signingWellKnownIdentities}, expected ${otherSideSession}"
|
||||
}
|
||||
val signed = stx.sigs.map { it.by }
|
||||
val allSigners = stx.tx.requiredSigningKeys
|
||||
@ -266,9 +275,9 @@ abstract class SignTransactionFlow(val otherParty: Party,
|
||||
@Throws(FlowException::class)
|
||||
abstract protected fun checkTransaction(stx: SignedTransaction)
|
||||
|
||||
@Suspendable private fun checkMySignatureRequired(stx: SignedTransaction, signingKey: PublicKey) {
|
||||
require(signingKey in stx.tx.requiredSigningKeys) {
|
||||
"Party is not a participant for any of the input states of transaction ${stx.id}"
|
||||
@Suspendable private fun checkMySignaturesRequired(stx: SignedTransaction, signingKeys: Iterable<PublicKey>) {
|
||||
require(signingKeys.all { it in stx.tx.requiredSigningKeys }) {
|
||||
"A signature was requested for a key that isn't part of the required signing keys for transaction ${stx.id}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ package net.corda.core.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
@ -87,13 +86,13 @@ object ContractUpgradeFlow {
|
||||
// TODO: We need a much faster way of finding our key in the transaction
|
||||
val myKey = serviceHub.keyManagementService.filterMyKeys(participantKeys).single()
|
||||
val stx = serviceHub.signInitialTransaction(baseTx, myKey)
|
||||
return AbstractStateReplacementFlow.UpgradeTx(stx, participantKeys, myKey)
|
||||
return AbstractStateReplacementFlow.UpgradeTx(stx)
|
||||
}
|
||||
}
|
||||
|
||||
@StartableByRPC
|
||||
@InitiatedBy(ContractUpgradeFlow.Initiator::class)
|
||||
class Acceptor(otherSide: Party) : AbstractStateReplacementFlow.Acceptor<Class<out UpgradedContract<ContractState, *>>>(otherSide) {
|
||||
class Acceptor(otherSide: FlowSession) : AbstractStateReplacementFlow.Acceptor<Class<out UpgradedContract<ContractState, *>>>(otherSide) {
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@ -133,7 +132,7 @@ object ContractUpgradeFlow {
|
||||
val proposedTx = stx.tx
|
||||
val expectedTx = ContractUpgradeFlow.Initiator.assembleBareTx(oldStateAndRef, proposal.modification, proposedTx.privacySalt).toWireTransaction()
|
||||
requireThat {
|
||||
"The instigator is one of the participants" using (otherSide in oldStateAndRef.state.data.participants)
|
||||
"The instigator is one of the participants" using (initiatingSession.counterparty in oldStateAndRef.state.data.participants)
|
||||
"The proposed upgrade ${proposal.modification.javaClass} is a trusted upgrade path" using (proposal.modification.name == authorisedUpgrade)
|
||||
"The proposed tx matches the expected tx for this upgrade" using (proposedTx == expectedTx)
|
||||
}
|
||||
|
@ -1,46 +1,35 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TransactionState
|
||||
import net.corda.core.crypto.isFulfilledBy
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.ResolveTransactionsFlow
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.NonEmptySet
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.toNonEmptySet
|
||||
|
||||
/**
|
||||
* Verifies the given transactions, then sends them to the named notary. If the notary agrees that the transactions
|
||||
* are acceptable then they are from that point onwards committed to the ledger, and will be written through to the
|
||||
* vault. Additionally they will be distributed to the parties reflected in the participants list of the states.
|
||||
* Verifies the given transaction, then sends it to the named notary. If the notary agrees that the transaction
|
||||
* is acceptable then it is from that point onwards committed to the ledger, and will be written through to the
|
||||
* vault. Additionally it will be distributed to the parties reflected in the participants list of the states.
|
||||
*
|
||||
* The transactions will be topologically sorted before commitment to ensure that dependencies are committed before
|
||||
* dependers, so you don't need to do this yourself.
|
||||
* 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.
|
||||
*
|
||||
* The transactions are expected to have already been resolved: if their dependencies are not available in local
|
||||
* storage or within the given set, verification will fail. They 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
|
||||
* from the contract-given set of participants.
|
||||
*
|
||||
* If specified, the extra recipients are sent all the given transactions. The base set of parties to inform of each
|
||||
* transaction are calculated on a per transaction basis from the contract-given set of participants.
|
||||
* The flow returns the same transaction but with the additional signatures from the notary.
|
||||
*
|
||||
* The flow returns the same transactions, in the same order, with the additional signatures.
|
||||
*
|
||||
* @param transactions What to commit.
|
||||
* @param transaction What to commit.
|
||||
* @param extraRecipients A list of additional participants to inform of the transaction.
|
||||
*/
|
||||
open class FinalityFlow(val transactions: Iterable<SignedTransaction>,
|
||||
@InitiatingFlow
|
||||
class FinalityFlow(val transaction: SignedTransaction,
|
||||
private val extraRecipients: Set<Party>,
|
||||
override val progressTracker: ProgressTracker) : FlowLogic<List<SignedTransaction>>() {
|
||||
constructor(transaction: SignedTransaction, extraParticipants: Set<Party>) : this(listOf(transaction), extraParticipants, tracker())
|
||||
constructor(transaction: SignedTransaction) : this(listOf(transaction), emptySet(), tracker())
|
||||
constructor(transaction: SignedTransaction, progressTracker: ProgressTracker) : this(listOf(transaction), emptySet(), progressTracker)
|
||||
override val progressTracker: ProgressTracker) : FlowLogic<SignedTransaction>() {
|
||||
constructor(transaction: SignedTransaction, extraParticipants: Set<Party>) : this(transaction, extraParticipants, tracker())
|
||||
constructor(transaction: SignedTransaction) : this(transaction, emptySet(), tracker())
|
||||
constructor(transaction: SignedTransaction, progressTracker: ProgressTracker) : this(transaction, emptySet(), progressTracker)
|
||||
|
||||
companion object {
|
||||
object NOTARISING : ProgressTracker.Step("Requesting signature by notary service") {
|
||||
@ -49,52 +38,41 @@ open class FinalityFlow(val transactions: Iterable<SignedTransaction>,
|
||||
|
||||
object BROADCASTING : ProgressTracker.Step("Broadcasting transaction to participants")
|
||||
|
||||
// TODO: Make all tracker() methods @JvmStatic
|
||||
@JvmStatic
|
||||
fun tracker() = ProgressTracker(NOTARISING, BROADCASTING)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
@Throws(NotaryException::class)
|
||||
override fun call(): List<SignedTransaction> {
|
||||
override fun call(): SignedTransaction {
|
||||
// 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.
|
||||
//
|
||||
// Lookup the resolved transactions and use them to map each signed transaction to the list of participants.
|
||||
// Then send to the notary if needed, record locally and distribute.
|
||||
val parties = getPartiesToSend(verifyTx())
|
||||
progressTracker.currentStep = NOTARISING
|
||||
val notarisedTxns: List<Pair<SignedTransaction, Set<Party>>> = resolveDependenciesOf(transactions)
|
||||
.map { (stx, ltx) -> Pair(notariseAndRecord(stx), lookupParties(ltx)) }
|
||||
val notarised = notariseAndRecord()
|
||||
|
||||
// Each transaction has its own set of recipients, but extra recipients get them all.
|
||||
progressTracker.currentStep = BROADCASTING
|
||||
for ((stx, parties) in notarisedTxns) {
|
||||
val participants = (parties + extraRecipients).filter { !serviceHub.myInfo.isLegalIdentity(it) }.toSet()
|
||||
if (participants.isNotEmpty()) {
|
||||
broadcastTransaction(stx, participants.toNonEmptySet())
|
||||
for (party in parties) {
|
||||
if (!serviceHub.myInfo.isLegalIdentity(party)) {
|
||||
val session = initiateFlow(party)
|
||||
subFlow(SendTransactionFlow(session, notarised))
|
||||
}
|
||||
}
|
||||
return notarisedTxns.map { it.first }
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast a transaction to the participants. By default calls [BroadcastTransactionFlow], however can be
|
||||
* overridden for more complex transaction delivery protocols (for example where not all parties know each other).
|
||||
*
|
||||
* @param participants the participants to send the transaction to. This is expected to include extra participants
|
||||
* and exclude the local node.
|
||||
*/
|
||||
@Suspendable
|
||||
open protected fun broadcastTransaction(stx: SignedTransaction, participants: NonEmptySet<Party>) {
|
||||
subFlow(BroadcastTransactionFlow(stx, participants))
|
||||
return notarised
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun notariseAndRecord(stx: SignedTransaction): SignedTransaction {
|
||||
val notarised = if (needsNotarySignature(stx)) {
|
||||
val notarySignatures = subFlow(NotaryFlow.Client(stx))
|
||||
stx + notarySignatures
|
||||
private fun notariseAndRecord(): SignedTransaction {
|
||||
val notarised = if (needsNotarySignature(transaction)) {
|
||||
val notarySignatures = subFlow(NotaryFlow.Client(transaction))
|
||||
transaction + notarySignatures
|
||||
} else {
|
||||
stx
|
||||
transaction
|
||||
}
|
||||
serviceHub.recordTransactions(notarised)
|
||||
return notarised
|
||||
@ -112,47 +90,17 @@ open class FinalityFlow(val transactions: Iterable<SignedTransaction>,
|
||||
return !(notaryKey?.isFulfilledBy(signers) ?: false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the parties involved in a transaction.
|
||||
*
|
||||
* The default implementation throws an exception if an unknown party is encountered.
|
||||
*/
|
||||
open protected fun lookupParties(ltx: LedgerTransaction): Set<Party> {
|
||||
// Calculate who is meant to see the results based on the participants involved.
|
||||
return extractParticipants(ltx).map {
|
||||
serviceHub.identityService.partyFromAnonymous(it)
|
||||
?: throw IllegalArgumentException("Could not resolve well known identity of participant $it")
|
||||
}.toSet()
|
||||
private fun getPartiesToSend(ltx: LedgerTransaction): Set<Party> {
|
||||
val participants = ltx.outputStates.flatMap { it.participants } + ltx.inputStates.flatMap { it.participants }
|
||||
return serviceHub.groupAbstractPartyByWellKnownParty(participants).keys + extraRecipients
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to extract all participants from a ledger transaction. Intended to help implement [lookupParties]
|
||||
* overriding functions.
|
||||
*/
|
||||
protected fun extractParticipants(ltx: LedgerTransaction): List<AbstractParty> {
|
||||
return ltx.outputStates.flatMap { it.participants } + ltx.inputStates.flatMap { it.participants }
|
||||
}
|
||||
|
||||
private fun resolveDependenciesOf(signedTransactions: Iterable<SignedTransaction>): List<Pair<SignedTransaction, LedgerTransaction>> {
|
||||
// Make sure the dependencies come before the dependers.
|
||||
val sorted = ResolveTransactionsFlow.topologicalSort(signedTransactions.toList())
|
||||
// Build a ServiceHub that consults the argument list as well as what's in local tx storage so uncommitted
|
||||
// transactions can depend on each other.
|
||||
val augmentedLookup = object : ServiceHub by serviceHub {
|
||||
val hashToTx = sorted.associateBy { it.id }
|
||||
override fun loadState(stateRef: StateRef): TransactionState<*> {
|
||||
val provided: TransactionState<ContractState>? = hashToTx[stateRef.txhash]?.let { it.tx.outputs[stateRef.index] }
|
||||
return provided ?: super.loadState(stateRef)
|
||||
}
|
||||
}
|
||||
// Load and verify each transaction.
|
||||
return sorted.map { stx ->
|
||||
val notary = stx.tx.notary
|
||||
// The notary signature(s) are allowed to be missing but no others.
|
||||
if (notary != null) stx.verifySignaturesExcept(notary.owningKey) else stx.verifyRequiredSignatures()
|
||||
val ltx = stx.toLedgerTransaction(augmentedLookup, false)
|
||||
ltx.verify()
|
||||
stx to ltx
|
||||
}
|
||||
private fun verifyTx(): LedgerTransaction {
|
||||
val notary = transaction.tx.notary
|
||||
// The notary signature(s) are allowed to be missing but no others.
|
||||
if (notary != null) transaction.verifySignaturesExcept(notary.owningKey) else transaction.verifyRequiredSignatures()
|
||||
val ltx = transaction.toLedgerTransaction(serviceHub, false)
|
||||
ltx.verify()
|
||||
return ltx
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import net.corda.core.CordaRuntimeException
|
||||
/**
|
||||
* Exception which can be thrown by a [FlowLogic] at any point in its logic to unexpectedly bring it to a permanent end.
|
||||
* The exception will propagate to all counterparty flows and will be thrown on their end the next time they wait on a
|
||||
* [FlowLogic.receive] or [FlowLogic.sendAndReceive]. Any flow which no longer needs to do a receive, or has already ended,
|
||||
* [FlowSession.receive] or [FlowSession.sendAndReceive]. Any flow which no longer needs to do a receive, or has already ended,
|
||||
* will not receive the exception (if this is required then have them wait for a confirmation message).
|
||||
*
|
||||
* [FlowException] (or a subclass) can be a valid expected response from a flow, particularly ones which act as a service.
|
||||
|
@ -137,11 +137,17 @@ abstract class FlowLogic<out T> {
|
||||
internal inline fun <reified R : Any> sendAndReceiveWithRetry(otherParty: Party, payload: Any): UntrustworthyData<R> {
|
||||
return stateMachine.sendAndReceive(R::class.java, otherParty, payload, flowUsedForSessions, retrySend = true)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
internal fun <R : Any> FlowSession.sendAndReceiveWithRetry(receiveType: Class<R>, payload: Any): UntrustworthyData<R> {
|
||||
return stateMachine.sendAndReceive(receiveType, counterparty, payload, flowUsedForSessions, retrySend = true)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
internal inline fun <reified R : Any> FlowSession.sendAndReceiveWithRetry(payload: Any): UntrustworthyData<R> {
|
||||
return stateMachine.sendAndReceive(R::class.java, counterparty, payload, flowUsedForSessions, retrySend = true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Suspends until the specified [otherParty] sends us a message of type [R].
|
||||
*
|
||||
|
@ -23,12 +23,12 @@ import net.corda.core.utilities.UntrustworthyData
|
||||
*
|
||||
* If it's an InitiatedBy flow:
|
||||
*
|
||||
* Change the constructor to take an initiatingSession: FlowSession instead of a counterparty: Party
|
||||
* Change the constructor to take an otherSideSession: FlowSession instead of a counterparty: Party
|
||||
* Then look for usages of the deprecated functions and change them to use the FlowSession
|
||||
* For example:
|
||||
* send(counterparty, something)
|
||||
* will become
|
||||
* initiatingSession.send(something)
|
||||
* otherSideSession.send(something)
|
||||
*/
|
||||
abstract class FlowSession {
|
||||
abstract val counterparty: Party
|
||||
|
@ -1,20 +0,0 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
|
||||
/**
|
||||
* Alternative finality flow which only does not attempt to take participants from the transaction, but instead all
|
||||
* participating parties must be provided manually.
|
||||
*
|
||||
* @param transactions What to commit.
|
||||
* @param recipients List of participants to inform of the transaction.
|
||||
*/
|
||||
class ManualFinalityFlow(transactions: Iterable<SignedTransaction>,
|
||||
recipients: Set<Party>,
|
||||
progressTracker: ProgressTracker) : FinalityFlow(transactions, recipients, progressTracker) {
|
||||
constructor(transaction: SignedTransaction, extraParticipants: Set<Party>) : this(listOf(transaction), extraParticipants, tracker())
|
||||
override fun lookupParties(ltx: LedgerTransaction): Set<Party> = emptySet()
|
||||
}
|
@ -43,7 +43,7 @@ class NotaryChangeFlow<out T : ContractState>(
|
||||
val mySignature = serviceHub.keyManagementService.sign(signableData, myKey)
|
||||
val stx = SignedTransaction(tx, listOf(mySignature))
|
||||
|
||||
return AbstractStateReplacementFlow.UpgradeTx(stx, participantKeys, myKey)
|
||||
return AbstractStateReplacementFlow.UpgradeTx(stx)
|
||||
}
|
||||
|
||||
/** Resolves the encumbrance state chain for the given [state] */
|
||||
|
@ -65,16 +65,17 @@ class NotaryFlow {
|
||||
}
|
||||
|
||||
val response = try {
|
||||
val session = initiateFlow(notaryParty)
|
||||
if (serviceHub.networkMapCache.isValidatingNotary(notaryParty)) {
|
||||
subFlow(SendTransactionWithRetry(notaryParty, stx))
|
||||
receive<List<TransactionSignature>>(notaryParty)
|
||||
subFlow(SendTransactionWithRetry(session, stx))
|
||||
session.receive<List<TransactionSignature>>()
|
||||
} else {
|
||||
val tx: Any = if (stx.isNotaryChangeTransaction()) {
|
||||
stx.notaryChangeTx
|
||||
} else {
|
||||
stx.buildFilteredTransaction(Predicate { it is StateRef || it is TimeWindow || it == notaryParty })
|
||||
}
|
||||
sendAndReceiveWithRetry(notaryParty, tx)
|
||||
session.sendAndReceiveWithRetry(tx)
|
||||
}
|
||||
} catch (e: NotaryException) {
|
||||
if (e.error is NotaryError.Conflict) {
|
||||
@ -99,10 +100,10 @@ class NotaryFlow {
|
||||
* The [SendTransactionWithRetry] flow is equivalent to [SendTransactionFlow] but using [sendAndReceiveWithRetry]
|
||||
* instead of [sendAndReceive], [SendTransactionWithRetry] is intended to be use by the notary client only.
|
||||
*/
|
||||
private class SendTransactionWithRetry(otherSide: Party, stx: SignedTransaction) : SendTransactionFlow(otherSide, stx) {
|
||||
private class SendTransactionWithRetry(otherSideSession: FlowSession, stx: SignedTransaction) : SendTransactionFlow(otherSideSession, stx) {
|
||||
@Suspendable
|
||||
override fun sendPayloadAndReceiveDataRequest(otherSide: Party, payload: Any): UntrustworthyData<FetchDataFlow.Request> {
|
||||
return sendAndReceiveWithRetry(otherSide, payload)
|
||||
override fun sendPayloadAndReceiveDataRequest(otherSideSession: FlowSession, payload: Any): UntrustworthyData<FetchDataFlow.Request> {
|
||||
return otherSideSession.sendAndReceiveWithRetry(payload)
|
||||
}
|
||||
}
|
||||
|
||||
@ -115,14 +116,14 @@ class NotaryFlow {
|
||||
* Additional transaction validation logic can be added when implementing [receiveAndVerifyTx].
|
||||
*/
|
||||
// See AbstractStateReplacementFlow.Acceptor for why it's Void?
|
||||
abstract class Service(val otherSide: Party, val service: TrustedAuthorityNotaryService) : FlowLogic<Void?>() {
|
||||
abstract class Service(val otherSideSession: FlowSession, val service: TrustedAuthorityNotaryService) : FlowLogic<Void?>() {
|
||||
|
||||
@Suspendable
|
||||
override fun call(): Void? {
|
||||
val (id, inputs, timeWindow, notary) = receiveAndVerifyTx()
|
||||
checkNotary(notary)
|
||||
service.validateTimeWindow(timeWindow)
|
||||
service.commitInputStates(inputs, id, otherSide)
|
||||
service.commitInputStates(inputs, id, otherSideSession.counterparty)
|
||||
signAndSendResponse(id)
|
||||
return null
|
||||
}
|
||||
@ -144,7 +145,7 @@ class NotaryFlow {
|
||||
@Suspendable
|
||||
private fun signAndSendResponse(txId: SecureHash) {
|
||||
val signature = service.sign(txId)
|
||||
send(otherSide, listOf(signature))
|
||||
otherSideSession.send(listOf(signature))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ package net.corda.core.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.ResolveTransactionsFlow
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.unwrap
|
||||
@ -11,18 +10,26 @@ import java.security.SignatureException
|
||||
/**
|
||||
* The [ReceiveTransactionFlow] should be called in response to the [SendTransactionFlow].
|
||||
*
|
||||
* This flow is a combination of [receive], resolve and [SignedTransaction.verify]. This flow will receive the [SignedTransaction]
|
||||
* and perform the resolution back-and-forth required to check the dependencies and download any missing attachments.
|
||||
* The flow will return the [SignedTransaction] after it is resolved and then verified using [SignedTransaction.verify].
|
||||
* This flow is a combination of [FlowSession.receive], resolve and [SignedTransaction.verify]. This flow will receive the
|
||||
* [SignedTransaction] and perform the resolution back-and-forth required to check the dependencies and download any missing
|
||||
* attachments. The flow will return the [SignedTransaction] after it is resolved and then verified using [SignedTransaction.verify].
|
||||
*
|
||||
* @param otherSideSession session to the other side which is calling [SendTransactionFlow].
|
||||
* @param checkSufficientSignatures if true checks all required signatures are present. See [SignedTransaction.verify].
|
||||
*/
|
||||
class ReceiveTransactionFlow
|
||||
@JvmOverloads
|
||||
constructor(private val otherParty: Party, private val checkSufficientSignatures: Boolean = true) : FlowLogic<SignedTransaction>() {
|
||||
class ReceiveTransactionFlow(private val otherSideSession: FlowSession,
|
||||
private val checkSufficientSignatures: Boolean) : FlowLogic<SignedTransaction>() {
|
||||
/** Receives a [SignedTransaction] from [otherSideSession], verifies it and then records it in the vault. */
|
||||
constructor(otherSideSession: FlowSession) : this(otherSideSession, true)
|
||||
|
||||
@Suspendable
|
||||
@Throws(SignatureException::class, AttachmentResolutionException::class, TransactionResolutionException::class, TransactionVerificationException::class)
|
||||
@Throws(SignatureException::class,
|
||||
AttachmentResolutionException::class,
|
||||
TransactionResolutionException::class,
|
||||
TransactionVerificationException::class)
|
||||
override fun call(): SignedTransaction {
|
||||
return receive<SignedTransaction>(otherParty).unwrap {
|
||||
subFlow(ResolveTransactionsFlow(it, otherParty))
|
||||
return otherSideSession.receive<SignedTransaction>().unwrap {
|
||||
subFlow(ResolveTransactionsFlow(it, otherSideSession))
|
||||
it.verify(serviceHub, checkSufficientSignatures)
|
||||
it
|
||||
}
|
||||
@ -32,16 +39,16 @@ constructor(private val otherParty: Party, private val checkSufficientSignatures
|
||||
/**
|
||||
* The [ReceiveStateAndRefFlow] should be called in response to the [SendStateAndRefFlow].
|
||||
*
|
||||
* This flow is a combination of [receive] and resolve. This flow will receive a list of [StateAndRef]
|
||||
* This flow is a combination of [FlowSession.receive] and resolve. This flow will receive a list of [StateAndRef]
|
||||
* and perform the resolution back-and-forth required to check the dependencies.
|
||||
* The flow will return the list of [StateAndRef] after it is resolved.
|
||||
*/
|
||||
// @JvmSuppressWildcards is used to suppress wildcards in return type when calling `subFlow(new ReceiveStateAndRef<T>(otherParty))` in java.
|
||||
class ReceiveStateAndRefFlow<out T : ContractState>(private val otherParty: Party) : FlowLogic<@JvmSuppressWildcards List<StateAndRef<T>>>() {
|
||||
class ReceiveStateAndRefFlow<out T : ContractState>(private val otherSideSession: FlowSession) : FlowLogic<@JvmSuppressWildcards List<StateAndRef<T>>>() {
|
||||
@Suspendable
|
||||
override fun call(): List<StateAndRef<T>> {
|
||||
return receive<List<StateAndRef<T>>>(otherParty).unwrap {
|
||||
subFlow(ResolveTransactionsFlow(it.map { it.ref.txhash }.toSet(), otherParty))
|
||||
return otherSideSession.receive<List<StateAndRef<T>>>().unwrap {
|
||||
subFlow(ResolveTransactionsFlow(it.map { it.ref.txhash }.toSet(), otherSideSession))
|
||||
it
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ package net.corda.core.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.FetchDataFlow
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.unwrap
|
||||
@ -14,9 +13,9 @@ import net.corda.core.utilities.unwrap
|
||||
* to check the dependencies and download any missing attachments.
|
||||
*
|
||||
* @param otherSide the target party.
|
||||
* @param stx the [SignedTransaction] being sent to the [otherSide].
|
||||
* @param stx the [SignedTransaction] being sent to the [otherSideSession].
|
||||
*/
|
||||
open class SendTransactionFlow(otherSide: Party, stx: SignedTransaction) : DataVendingFlow(otherSide, stx)
|
||||
open class SendTransactionFlow(otherSide: FlowSession, stx: SignedTransaction) : DataVendingFlow(otherSide, stx)
|
||||
|
||||
/**
|
||||
* The [SendStateAndRefFlow] should be used to send a list of input [StateAndRef] to another peer that wishes to verify
|
||||
@ -24,14 +23,14 @@ open class SendTransactionFlow(otherSide: Party, stx: SignedTransaction) : DataV
|
||||
* at the right point in the conversation to receive the input state and ref and perform the resolution back-and-forth
|
||||
* required to check the dependencies.
|
||||
*
|
||||
* @param otherSide the target party.
|
||||
* @param stateAndRefs the list of [StateAndRef] being sent to the [otherSide].
|
||||
* @param otherSideSession the target session.
|
||||
* @param stateAndRefs the list of [StateAndRef] being sent to the [otherSideSession].
|
||||
*/
|
||||
open class SendStateAndRefFlow(otherSide: Party, stateAndRefs: List<StateAndRef<*>>) : DataVendingFlow(otherSide, stateAndRefs)
|
||||
open class SendStateAndRefFlow(otherSideSession: FlowSession, stateAndRefs: List<StateAndRef<*>>) : DataVendingFlow(otherSideSession, stateAndRefs)
|
||||
|
||||
sealed class DataVendingFlow(val otherSide: Party, val payload: Any) : FlowLogic<Void?>() {
|
||||
sealed class DataVendingFlow(val otherSideSession: FlowSession, val payload: Any) : FlowLogic<Void?>() {
|
||||
@Suspendable
|
||||
protected open fun sendPayloadAndReceiveDataRequest(otherSide: Party, payload: Any) = sendAndReceive<FetchDataFlow.Request>(otherSide, payload)
|
||||
protected open fun sendPayloadAndReceiveDataRequest(otherSideSession: FlowSession, payload: Any) = otherSideSession.sendAndReceive<FetchDataFlow.Request>(payload)
|
||||
|
||||
@Suspendable
|
||||
protected open fun verifyDataRequest(dataRequest: FetchDataFlow.Request.Data) {
|
||||
@ -42,11 +41,11 @@ sealed class DataVendingFlow(val otherSide: Party, val payload: Any) : FlowLogic
|
||||
override fun call(): Void? {
|
||||
// The first payload will be the transaction data, subsequent payload will be the transaction/attachment data.
|
||||
var payload = payload
|
||||
// This loop will receive [FetchDataFlow.Request] continuously until the `otherSide` has all the data they need
|
||||
// to resolve the transaction, a [FetchDataFlow.EndRequest] will be sent from the `otherSide` to indicate end of
|
||||
// This loop will receive [FetchDataFlow.Request] continuously until the `otherSideSession` has all the data they need
|
||||
// to resolve the transaction, a [FetchDataFlow.EndRequest] will be sent from the `otherSideSession` to indicate end of
|
||||
// data request.
|
||||
while (true) {
|
||||
val dataRequest = sendPayloadAndReceiveDataRequest(otherSide, payload).unwrap { request ->
|
||||
val dataRequest = sendPayloadAndReceiveDataRequest(otherSideSession, payload).unwrap { request ->
|
||||
when (request) {
|
||||
is FetchDataFlow.Request.Data -> {
|
||||
// Security TODO: Check for abnormally large or malformed data requests
|
||||
|
@ -7,7 +7,7 @@ import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.internal.FetchDataFlow.DownloadedVsRequestedDataMismatch
|
||||
import net.corda.core.internal.FetchDataFlow.HashNotFound
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
@ -38,7 +38,7 @@ import java.util.*
|
||||
*/
|
||||
sealed class FetchDataFlow<T : NamedByHash, in W : Any>(
|
||||
protected val requests: Set<SecureHash>,
|
||||
protected val otherSide: Party,
|
||||
protected val otherSideSession: FlowSession,
|
||||
protected val dataType: DataType) : FlowLogic<FetchDataFlow.Result<T>>() {
|
||||
|
||||
@CordaSerializable
|
||||
@ -72,7 +72,7 @@ sealed class FetchDataFlow<T : NamedByHash, in W : Any>(
|
||||
return if (toFetch.isEmpty()) {
|
||||
Result(fromDisk, emptyList())
|
||||
} else {
|
||||
logger.info("Requesting ${toFetch.size} dependency(s) for verification from ${otherSide.name}")
|
||||
logger.info("Requesting ${toFetch.size} dependency(s) for verification from ${otherSideSession.counterparty.name}")
|
||||
|
||||
// TODO: Support "large message" response streaming so response sizes are not limited by RAM.
|
||||
// We can then switch to requesting items in large batches to minimise the latency penalty.
|
||||
@ -85,11 +85,11 @@ sealed class FetchDataFlow<T : NamedByHash, in W : Any>(
|
||||
for (hash in toFetch) {
|
||||
// We skip the validation here (with unwrap { it }) because we will do it below in validateFetchResponse.
|
||||
// The only thing checked is the object type. It is a protocol violation to send results out of order.
|
||||
maybeItems += sendAndReceive<List<W>>(otherSide, Request.Data(NonEmptySet.of(hash), dataType)).unwrap { it }
|
||||
maybeItems += otherSideSession.sendAndReceive<List<W>>(Request.Data(NonEmptySet.of(hash), dataType)).unwrap { it }
|
||||
}
|
||||
// Check for a buggy/malicious peer answering with something that we didn't ask for.
|
||||
val downloaded = validateFetchResponse(UntrustworthyData(maybeItems), toFetch)
|
||||
logger.info("Fetched ${downloaded.size} elements from ${otherSide.name}")
|
||||
logger.info("Fetched ${downloaded.size} elements from ${otherSideSession.counterparty.name}")
|
||||
maybeWriteToDisk(downloaded)
|
||||
Result(fromDisk, downloaded)
|
||||
}
|
||||
@ -140,7 +140,7 @@ sealed class FetchDataFlow<T : NamedByHash, in W : Any>(
|
||||
* attachments are saved to local storage automatically.
|
||||
*/
|
||||
class FetchAttachmentsFlow(requests: Set<SecureHash>,
|
||||
otherSide: Party) : FetchDataFlow<Attachment, ByteArray>(requests, otherSide, DataType.ATTACHMENT) {
|
||||
otherSide: FlowSession) : FetchDataFlow<Attachment, ByteArray>(requests, otherSide, DataType.ATTACHMENT) {
|
||||
|
||||
override fun load(txid: SecureHash): Attachment? = serviceHub.attachments.openAttachment(txid)
|
||||
|
||||
@ -171,7 +171,7 @@ class FetchAttachmentsFlow(requests: Set<SecureHash>,
|
||||
* results in a [FetchDataFlow.HashNotFound] exception. Note that returned transactions are not inserted into
|
||||
* the database, because it's up to the caller to actually verify the transactions are valid.
|
||||
*/
|
||||
class FetchTransactionsFlow(requests: Set<SecureHash>, otherSide: Party) :
|
||||
class FetchTransactionsFlow(requests: Set<SecureHash>, otherSide: FlowSession) :
|
||||
FetchDataFlow<SignedTransaction, SignedTransaction>(requests, otherSide, DataType.TRANSACTION) {
|
||||
|
||||
override fun load(txid: SecureHash): SignedTransaction? = serviceHub.validatedTransactions.getTransaction(txid)
|
||||
|
@ -272,3 +272,5 @@ annotation class VisibleForTesting
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T, U : T> uncheckedCast(obj: T) = obj as U
|
||||
|
||||
fun <K, V> Iterable<Pair<K, V>>.toMultiMap(): Map<K, List<V>> = this.groupBy({ it.first }) { it.second }
|
@ -3,7 +3,7 @@ package net.corda.core.internal
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.exactAdd
|
||||
@ -17,14 +17,14 @@ import java.util.*
|
||||
* @return a list of verified [SignedTransaction] objects, in a depth-first order.
|
||||
*/
|
||||
class ResolveTransactionsFlow(private val txHashes: Set<SecureHash>,
|
||||
private val otherSide: Party) : FlowLogic<List<SignedTransaction>>() {
|
||||
private val otherSide: FlowSession) : FlowLogic<List<SignedTransaction>>() {
|
||||
/**
|
||||
* Resolves and validates the dependencies of the specified [signedTransaction]. Fetches the attachments, but does
|
||||
* *not* validate or store the [signedTransaction] itself.
|
||||
*
|
||||
* @return a list of verified [SignedTransaction] objects, in a depth-first order.
|
||||
*/
|
||||
constructor(signedTransaction: SignedTransaction, otherSide: Party) : this(dependencyIDs(signedTransaction), otherSide) {
|
||||
constructor(signedTransaction: SignedTransaction, otherSide: FlowSession) : this(dependencyIDs(signedTransaction), otherSide) {
|
||||
this.signedTransaction = signedTransaction
|
||||
}
|
||||
companion object {
|
||||
@ -82,7 +82,7 @@ class ResolveTransactionsFlow(private val txHashes: Set<SecureHash>,
|
||||
// Start fetching data.
|
||||
val newTxns = downloadDependencies(txHashes)
|
||||
fetchMissingAttachments(signedTransaction?.let { newTxns + it } ?: newTxns)
|
||||
send(otherSide, FetchDataFlow.Request.End)
|
||||
otherSide.send(FetchDataFlow.Request.End)
|
||||
// Finish fetching data.
|
||||
|
||||
val result = topologicalSort(newTxns)
|
||||
|
@ -5,8 +5,10 @@ import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.SignableData
|
||||
import net.corda.core.crypto.SignatureMetadata
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.internal.toMultiMap
|
||||
import net.corda.core.node.services.*
|
||||
import net.corda.core.serialization.SerializeAsToken
|
||||
import net.corda.core.transactions.FilteredTransaction
|
||||
@ -269,4 +271,75 @@ interface ServiceHub : ServicesForResolution {
|
||||
* @return A new [Connection]
|
||||
*/
|
||||
fun jdbcSession(): Connection
|
||||
|
||||
/**
|
||||
* Group each [PublicKey] by the well known party using the [ServiceHub.identityService], in preparation for
|
||||
* creating [FlowSession]s, for example.
|
||||
*
|
||||
* @param publicKeys the [PublicKey]s to group.
|
||||
* @param ignoreUnrecognisedParties if this is false, throw an exception if some of the [PublicKey]s cannot be mapped
|
||||
* to a [Party].
|
||||
* @return a map of well known [Party] to associated [PublicKey]s.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun groupPublicKeysByWellKnownParty(publicKeys: Collection<PublicKey>, ignoreUnrecognisedParties: Boolean): Map<Party, List<PublicKey>> =
|
||||
groupAbstractPartyByWellKnownParty(publicKeys.map { AnonymousParty(it) }, ignoreUnrecognisedParties).mapValues { it.value.map { it.owningKey } }
|
||||
|
||||
/**
|
||||
* Group each [PublicKey] by the well known party using the [ServiceHub.identityService], in preparation for
|
||||
* creating [FlowSession]s, for example. Throw an exception if some of the [PublicKey]s cannot be mapped
|
||||
* to a [Party].
|
||||
*
|
||||
* @param publicKeys the [PublicKey]s to group.
|
||||
* @return a map of well known [Party] to associated [PublicKey]s.
|
||||
*/
|
||||
// Cannot use @JvmOverloads in interface
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun groupPublicKeysByWellKnownParty(publicKeys: Collection<PublicKey>): Map<Party, List<PublicKey>> = groupPublicKeysByWellKnownParty(publicKeys, false)
|
||||
|
||||
/**
|
||||
* Group each [AbstractParty] by the well known party using the [ServiceHub.identityService], in preparation for
|
||||
* creating [FlowSession]s, for example.
|
||||
*
|
||||
* @param parties the [AbstractParty]s to group.
|
||||
* @param ignoreUnrecognisedParties if this is false, throw an exception if some of the [AbstractParty]s cannot be mapped
|
||||
* to a [Party].
|
||||
* @return a map of well known [Party] to associated [AbstractParty]s.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun groupAbstractPartyByWellKnownParty(parties: Collection<AbstractParty>, ignoreUnrecognisedParties: Boolean): Map<Party, List<AbstractParty>> {
|
||||
val partyToPublicKey: Iterable<Pair<Party, AbstractParty>> = parties.mapNotNull {
|
||||
(identityService.partyFromAnonymous(it) ?: if (ignoreUnrecognisedParties) return@mapNotNull null else throw IllegalArgumentException("Could not find Party for $it")) to it
|
||||
}
|
||||
return partyToPublicKey.toMultiMap()
|
||||
}
|
||||
|
||||
/**
|
||||
* Group each [AbstractParty] by the well known party using the [ServiceHub.identityService], in preparation for
|
||||
* creating [FlowSession]s, for example. Throw an exception if some of the [AbstractParty]s cannot be mapped
|
||||
* to a [Party].
|
||||
*
|
||||
* @param parties the [AbstractParty]s to group.
|
||||
* @return a map of well known [Party] to associated [AbstractParty]s.
|
||||
*/
|
||||
// Cannot use @JvmOverloads in interface
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun groupAbstractPartyByWellKnownParty(parties: Collection<AbstractParty>): Map<Party, List<AbstractParty>> {
|
||||
return groupAbstractPartyByWellKnownParty(parties, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove this node from a map of well known [Party]s.
|
||||
*
|
||||
* @return a new copy of the map, with the well known [Party] for this node removed.
|
||||
*/
|
||||
fun <T> excludeMe(map: Map<Party, T>): Map<Party, T> = map.filterKeys { !myInfo.isLegalIdentity(it) }
|
||||
|
||||
/**
|
||||
* Remove the [Party] associated with the notary of a [SignedTransaction] from the a map of [Party]s. It is a no-op
|
||||
* if the notary is null.
|
||||
*
|
||||
* @return a new copy of the map, with the well known [Party] for the notary removed.
|
||||
*/
|
||||
fun <T> excludeNotary(map: Map<Party, T>, stx: SignedTransaction): Map<Party, T> = map.filterKeys { it != stx.notary }
|
||||
}
|
||||
|
@ -3,10 +3,7 @@ package net.corda.core.node.services
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TimeWindow
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.NotaryError
|
||||
import net.corda.core.flows.NotaryException
|
||||
import net.corda.core.flows.NotaryFlow
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
@ -24,9 +21,9 @@ abstract class NotaryService : SingletonSerializeAsToken() {
|
||||
|
||||
/**
|
||||
* Produces a notary service flow which has the corresponding sends and receives as [NotaryFlow.Client].
|
||||
* @param otherParty client [Party] making the request
|
||||
* @param otherPartySession client [Party] making the request
|
||||
*/
|
||||
abstract fun createServiceFlow(otherParty: Party): FlowLogic<Void?>
|
||||
abstract fun createServiceFlow(otherPartySession: FlowSession): FlowLogic<Void?>
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -42,7 +42,7 @@ interface TransactionWithSignatures : NamedByHash {
|
||||
fun verifySignaturesExcept(vararg allowedToBeMissing: PublicKey) {
|
||||
checkSignaturesAreValid()
|
||||
|
||||
val needed = getMissingSignatures() - allowedToBeMissing
|
||||
val needed = getMissingSigners() - allowedToBeMissing
|
||||
if (needed.isNotEmpty())
|
||||
throw SignaturesMissingException(needed.toNonEmptySet(), getKeyDescriptions(needed), id)
|
||||
}
|
||||
@ -71,7 +71,10 @@ interface TransactionWithSignatures : NamedByHash {
|
||||
*/
|
||||
fun getKeyDescriptions(keys: Set<PublicKey>): List<String>
|
||||
|
||||
private fun getMissingSignatures(): Set<PublicKey> {
|
||||
/**
|
||||
* Return the [PublicKey]s for which we still need signatures.
|
||||
*/
|
||||
fun getMissingSigners(): Set<PublicKey> {
|
||||
val sigKeys = sigs.map { it.by }.toSet()
|
||||
// TODO Problem is that we can get single PublicKey wrapped as CompositeKey in allowedToBeMissing/mustSign
|
||||
// equals on CompositeKey won't catch this case (do we want to single PublicKey be equal to the same key wrapped in CompositeKey with threshold 1?)
|
||||
|
@ -80,8 +80,9 @@ public class FlowsInJavaTest {
|
||||
@Suspendable
|
||||
@Override
|
||||
public String call() throws FlowException {
|
||||
return receive(String.class, otherParty).unwrap(data -> {
|
||||
send(otherParty, "Something");
|
||||
FlowSession session = initiateFlow(otherParty);
|
||||
return session.receive(String.class).unwrap(data -> {
|
||||
session.send("Something");
|
||||
return data;
|
||||
});
|
||||
}
|
||||
@ -89,16 +90,16 @@ public class FlowsInJavaTest {
|
||||
|
||||
@InitiatedBy(SendInUnwrapFlow.class)
|
||||
private static class SendHelloAndThenReceive extends FlowLogic<String> {
|
||||
private final Party otherParty;
|
||||
private final FlowSession otherSide;
|
||||
|
||||
private SendHelloAndThenReceive(Party otherParty) {
|
||||
this.otherParty = otherParty;
|
||||
private SendHelloAndThenReceive(FlowSession otherParty) {
|
||||
this.otherSide = otherParty;
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
@Override
|
||||
public String call() throws FlowException {
|
||||
return sendAndReceive(String.class, otherParty, "Hello").unwrap(data -> data);
|
||||
return otherSide.sendAndReceive(String.class, "Hello").unwrap(data -> data);
|
||||
}
|
||||
}
|
||||
|
||||
@ -115,7 +116,8 @@ public class FlowsInJavaTest {
|
||||
@Suspendable
|
||||
@Override
|
||||
public Void call() throws FlowException {
|
||||
receive(Primitives.unwrap(receiveType), otherParty);
|
||||
FlowSession session = initiateFlow(otherParty);
|
||||
session.receive(Primitives.unwrap(receiveType));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -162,12 +162,15 @@ class AttachmentTests {
|
||||
@InitiatingFlow
|
||||
private class InitiatingFetchAttachmentsFlow(val otherSide: Party, val hashes: Set<SecureHash>) : FlowLogic<FetchDataFlow.Result<Attachment>>() {
|
||||
@Suspendable
|
||||
override fun call(): FetchDataFlow.Result<Attachment> = subFlow(FetchAttachmentsFlow(hashes, otherSide))
|
||||
override fun call(): FetchDataFlow.Result<Attachment> {
|
||||
val session = initiateFlow(otherSide)
|
||||
return subFlow(FetchAttachmentsFlow(hashes, session))
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(InitiatingFetchAttachmentsFlow::class)
|
||||
private class FetchAttachmentsResponse(val otherSide: Party) : FlowLogic<Void?>() {
|
||||
private class FetchAttachmentsResponse(val otherSideSession: FlowSession) : FlowLogic<Void?>() {
|
||||
@Suspendable
|
||||
override fun call() = subFlow(TestDataVendingFlow(otherSide))
|
||||
override fun call() = subFlow(TestDataVendingFlow(otherSideSession))
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.Command
|
||||
import net.corda.core.contracts.StateAndContract
|
||||
import net.corda.core.contracts.requireThat
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
@ -31,7 +30,6 @@ class CollectSignaturesFlowTests {
|
||||
lateinit var b: StartedNode<MockNetwork.MockNode>
|
||||
lateinit var c: StartedNode<MockNetwork.MockNode>
|
||||
lateinit var notary: Party
|
||||
val services = MockServices()
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
@ -61,12 +59,13 @@ class CollectSignaturesFlowTests {
|
||||
// "collectSignaturesFlow" and "SignTransactionFlow" can be used in practise.
|
||||
object TestFlow {
|
||||
@InitiatingFlow
|
||||
class Initiator(val state: DummyContract.MultiOwnerState, val otherParty: Party) : FlowLogic<SignedTransaction>() {
|
||||
class Initiator(private val state: DummyContract.MultiOwnerState, private val otherParty: Party) : FlowLogic<SignedTransaction>() {
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
send(otherParty, state)
|
||||
val session = initiateFlow(otherParty)
|
||||
session.send(state)
|
||||
|
||||
val flow = object : SignTransactionFlow(otherParty) {
|
||||
val flow = object : SignTransactionFlow(session) {
|
||||
@Suspendable override fun checkTransaction(stx: SignedTransaction) = requireThat {
|
||||
val tx = stx.tx
|
||||
val ltx = tx.toLedgerTransaction(serviceHub)
|
||||
@ -83,19 +82,19 @@ class CollectSignaturesFlowTests {
|
||||
}
|
||||
|
||||
@InitiatedBy(TestFlow.Initiator::class)
|
||||
class Responder(val otherParty: Party, val identities: Map<Party, AnonymousParty>) : FlowLogic<SignedTransaction>() {
|
||||
class Responder(private val initiatingSession: FlowSession) : FlowLogic<SignedTransaction>() {
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val state = receive<DummyContract.MultiOwnerState>(otherParty).unwrap { it }
|
||||
val state = initiatingSession.receive<DummyContract.MultiOwnerState>().unwrap { it }
|
||||
val notary = serviceHub.getDefaultNotary()
|
||||
|
||||
val myInputKeys = state.participants.map { it.owningKey }
|
||||
val myKeys = myInputKeys + (identities[ourIdentity] ?: ourIdentity).owningKey
|
||||
val command = Command(DummyContract.Commands.Create(), myInputKeys)
|
||||
val builder = TransactionBuilder(notary).withItems(StateAndContract(state, DUMMY_PROGRAM_ID), command)
|
||||
val ptx = serviceHub.signInitialTransaction(builder)
|
||||
val stx = subFlow(CollectSignaturesFlow(ptx, myKeys))
|
||||
return subFlow(FinalityFlow(stx)).single()
|
||||
val signature = subFlow(CollectSignatureFlow(ptx, initiatingSession, initiatingSession.counterparty.owningKey))
|
||||
val stx = ptx + signature
|
||||
return subFlow(FinalityFlow(stx))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -105,7 +104,7 @@ class CollectSignaturesFlowTests {
|
||||
// receiving off the wire.
|
||||
object TestFlowTwo {
|
||||
@InitiatingFlow
|
||||
class Initiator(val state: DummyContract.MultiOwnerState) : FlowLogic<SignedTransaction>() {
|
||||
class Initiator(private val state: DummyContract.MultiOwnerState) : FlowLogic<SignedTransaction>() {
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val notary = serviceHub.getDefaultNotary()
|
||||
@ -113,15 +112,16 @@ class CollectSignaturesFlowTests {
|
||||
val command = Command(DummyContract.Commands.Create(), myInputKeys)
|
||||
val builder = TransactionBuilder(notary).withItems(StateAndContract(state, DUMMY_PROGRAM_ID), command)
|
||||
val ptx = serviceHub.signInitialTransaction(builder)
|
||||
val stx = subFlow(CollectSignaturesFlow(ptx, myInputKeys))
|
||||
return subFlow(FinalityFlow(stx)).single()
|
||||
val sessions = serviceHub.excludeMe(serviceHub.groupAbstractPartyByWellKnownParty(state.owners)).map { initiateFlow(it.key) }
|
||||
val stx = subFlow(CollectSignaturesFlow(ptx, sessions, myInputKeys))
|
||||
return subFlow(FinalityFlow(stx))
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(TestFlowTwo.Initiator::class)
|
||||
class Responder(val otherParty: Party) : FlowLogic<SignedTransaction>() {
|
||||
@Suspendable override fun call(): SignedTransaction {
|
||||
val flow = object : SignTransactionFlow(otherParty) {
|
||||
class Responder(private val otherSideSession: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable override fun call() {
|
||||
val signFlow = object : SignTransactionFlow(otherSideSession) {
|
||||
@Suspendable override fun checkTransaction(stx: SignedTransaction) = requireThat {
|
||||
val tx = stx.tx
|
||||
val ltx = tx.toLedgerTransaction(serviceHub)
|
||||
@ -132,9 +132,8 @@ class CollectSignaturesFlowTests {
|
||||
}
|
||||
}
|
||||
|
||||
val stx = subFlow(flow)
|
||||
|
||||
return waitForLedgerCommit(stx.id)
|
||||
val stx = subFlow(signFlow)
|
||||
waitForLedgerCommit(stx.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -164,7 +163,7 @@ class CollectSignaturesFlowTests {
|
||||
fun `no need to collect any signatures`() {
|
||||
val onePartyDummyContract = DummyContract.generateInitial(1337, notary, a.info.chooseIdentity().ref(1))
|
||||
val ptx = a.services.signInitialTransaction(onePartyDummyContract)
|
||||
val flow = a.services.startFlow(CollectSignaturesFlow(ptx))
|
||||
val flow = a.services.startFlow(CollectSignaturesFlow(ptx, emptySet()))
|
||||
mockNet.runNetwork()
|
||||
val result = flow.resultFuture.getOrThrow()
|
||||
result.verifyRequiredSignatures()
|
||||
@ -177,7 +176,7 @@ class CollectSignaturesFlowTests {
|
||||
val onePartyDummyContract = DummyContract.generateInitial(1337, notary, a.info.chooseIdentity().ref(1))
|
||||
val miniCorpServices = MockServices(MINI_CORP_KEY)
|
||||
val ptx = miniCorpServices.signInitialTransaction(onePartyDummyContract)
|
||||
val flow = a.services.startFlow(CollectSignaturesFlow(ptx))
|
||||
val flow = a.services.startFlow(CollectSignaturesFlow(ptx, emptySet()))
|
||||
mockNet.runNetwork()
|
||||
assertFailsWith<IllegalArgumentException>("The Initiator of CollectSignaturesFlow must have signed the transaction.") {
|
||||
flow.resultFuture.getOrThrow()
|
||||
@ -192,7 +191,7 @@ class CollectSignaturesFlowTests {
|
||||
b.info.chooseIdentity().ref(3))
|
||||
val signedByA = a.services.signInitialTransaction(twoPartyDummyContract)
|
||||
val signedByBoth = b.services.addSignature(signedByA)
|
||||
val flow = a.services.startFlow(CollectSignaturesFlow(signedByBoth))
|
||||
val flow = a.services.startFlow(CollectSignaturesFlow(signedByBoth, emptySet()))
|
||||
mockNet.runNetwork()
|
||||
val result = flow.resultFuture.getOrThrow()
|
||||
println(result.tx)
|
||||
|
@ -21,15 +21,10 @@ import net.corda.node.internal.CordaRPCOpsImpl
|
||||
import net.corda.node.internal.StartedNode
|
||||
import net.corda.node.services.FlowPermissions.Companion.startFlowPermission
|
||||
import net.corda.nodeapi.User
|
||||
import net.corda.testing.RPCDriverExposedDSLInterface
|
||||
import net.corda.testing.chooseIdentity
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.contracts.DummyContractV2
|
||||
import net.corda.testing.getDefaultNotary
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.rpcDriver
|
||||
import net.corda.testing.rpcTestUser
|
||||
import net.corda.testing.startRpcClient
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
@ -63,7 +58,6 @@ class ContractUpgradeFlowTest {
|
||||
b.database.transaction {
|
||||
b.services.identityService.verifyAndRegisterIdentity(nodeIdentity)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@After
|
||||
@ -78,7 +72,7 @@ class ContractUpgradeFlowTest {
|
||||
val signedByA = a.services.signInitialTransaction(twoPartyDummyContract)
|
||||
val stx = b.services.addSignature(signedByA)
|
||||
|
||||
a.services.startFlow(FinalityFlow(stx, setOf(a.info.chooseIdentity(), b.info.chooseIdentity())))
|
||||
a.services.startFlow(FinalityFlow(stx, setOf(b.info.chooseIdentity())))
|
||||
mockNet.runNetwork()
|
||||
|
||||
val atx = a.database.transaction { a.services.validatedTransactions.getTransaction(stx.id) }
|
||||
@ -157,7 +151,7 @@ class ContractUpgradeFlowTest {
|
||||
))
|
||||
val rpcA = startProxy(a, user)
|
||||
val rpcB = startProxy(b, user)
|
||||
val handle = rpcA.startFlow(::FinalityInvoker, stx, setOf(a.info.chooseIdentity(), b.info.chooseIdentity()))
|
||||
val handle = rpcA.startFlow(::FinalityInvoker, stx, setOf(b.info.chooseIdentity()))
|
||||
mockNet.runNetwork()
|
||||
handle.returnValue.getOrThrow()
|
||||
|
||||
@ -257,9 +251,9 @@ class ContractUpgradeFlowTest {
|
||||
}
|
||||
|
||||
@StartableByRPC
|
||||
class FinalityInvoker(val transaction: SignedTransaction,
|
||||
val extraRecipients: Set<Party>) : FlowLogic<List<SignedTransaction>>() {
|
||||
class FinalityInvoker(private val transaction: SignedTransaction,
|
||||
private val extraRecipients: Set<Party>) : FlowLogic<SignedTransaction>() {
|
||||
@Suspendable
|
||||
override fun call(): List<SignedTransaction> = subFlow(FinalityFlow(transaction, extraRecipients))
|
||||
override fun call(): SignedTransaction = subFlow(FinalityFlow(transaction, extraRecipients))
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,13 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.Issued
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.finance.GBP
|
||||
import net.corda.finance.POUNDS
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.testing.ALICE
|
||||
import net.corda.finance.issuedBy
|
||||
import net.corda.node.internal.StartedNode
|
||||
import net.corda.testing.ALICE
|
||||
import net.corda.testing.chooseIdentity
|
||||
import net.corda.testing.getDefaultNotary
|
||||
import net.corda.testing.node.MockNetwork
|
||||
@ -44,14 +43,13 @@ class FinalityFlowTests {
|
||||
|
||||
@Test
|
||||
fun `finalise a simple transaction`() {
|
||||
val amount = Amount(1000, Issued(nodeA.info.chooseIdentity().ref(0), GBP))
|
||||
val amount = 1000.POUNDS.issuedBy(nodeA.info.chooseIdentity().ref(0))
|
||||
val builder = TransactionBuilder(notary)
|
||||
Cash().generateIssue(builder, amount, nodeB.info.chooseIdentity(), notary)
|
||||
val stx = nodeA.services.signInitialTransaction(builder)
|
||||
val flow = nodeA.services.startFlow(FinalityFlow(stx))
|
||||
mockNet.runNetwork()
|
||||
val result = flow.resultFuture.getOrThrow()
|
||||
val notarisedTx = result.single()
|
||||
val notarisedTx = flow.resultFuture.getOrThrow()
|
||||
notarisedTx.verifyRequiredSignatures()
|
||||
val transactionSeenByB = nodeB.services.database.transaction {
|
||||
nodeB.services.validatedTransactions.getTransaction(notarisedTx.id)
|
||||
@ -61,7 +59,7 @@ class FinalityFlowTests {
|
||||
|
||||
@Test
|
||||
fun `reject a transaction with unknown parties`() {
|
||||
val amount = Amount(1000, Issued(nodeA.info.chooseIdentity().ref(0), GBP))
|
||||
val amount = 1000.POUNDS.issuedBy(nodeA.info.chooseIdentity().ref(0))
|
||||
val fakeIdentity = ALICE // Alice isn't part of this network, so node A won't recognise them
|
||||
val builder = TransactionBuilder(notary)
|
||||
Cash().generateIssue(builder, amount, fakeIdentity, notary)
|
||||
|
@ -1,67 +0,0 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.Issued
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.finance.GBP
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.node.internal.StartedNode
|
||||
import net.corda.testing.chooseIdentity
|
||||
import net.corda.testing.getDefaultNotary
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.MockServices
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class ManualFinalityFlowTests {
|
||||
lateinit var mockNet: MockNetwork
|
||||
lateinit var nodeA: StartedNode<MockNetwork.MockNode>
|
||||
lateinit var nodeB: StartedNode<MockNetwork.MockNode>
|
||||
lateinit var nodeC: StartedNode<MockNetwork.MockNode>
|
||||
lateinit var notary: Party
|
||||
val services = MockServices()
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mockNet = MockNetwork()
|
||||
val nodes = mockNet.createSomeNodes(3)
|
||||
nodeA = nodes.partyNodes[0]
|
||||
nodeB = nodes.partyNodes[1]
|
||||
nodeC = nodes.partyNodes[2]
|
||||
mockNet.runNetwork()
|
||||
nodeA.internals.ensureRegistered()
|
||||
notary = nodeA.services.getDefaultNotary()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
mockNet.stopNodes()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `finalise a simple transaction`() {
|
||||
val amount = Amount(1000, Issued(nodeA.info.chooseIdentity().ref(0), GBP))
|
||||
val builder = TransactionBuilder(notary)
|
||||
Cash().generateIssue(builder, amount, nodeB.info.chooseIdentity(), notary)
|
||||
val stx = nodeA.services.signInitialTransaction(builder)
|
||||
val flow = nodeA.services.startFlow(ManualFinalityFlow(stx, setOf(nodeC.info.chooseIdentity())))
|
||||
mockNet.runNetwork()
|
||||
val result = flow.resultFuture.getOrThrow()
|
||||
val notarisedTx = result.single()
|
||||
notarisedTx.verifyRequiredSignatures()
|
||||
// We override the participants, so node C will get a copy despite not being involved, and B won't
|
||||
val transactionSeenByB = nodeB.services.database.transaction {
|
||||
nodeB.services.validatedTransactions.getTransaction(notarisedTx.id)
|
||||
}
|
||||
assertNull(transactionSeenByB)
|
||||
val transactionSeenByC = nodeC.services.database.transaction {
|
||||
nodeC.services.validatedTransactions.getTransaction(notarisedTx.id)
|
||||
}
|
||||
assertEquals(notarisedTx, transactionSeenByC)
|
||||
}
|
||||
}
|
@ -1,19 +1,18 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.FetchDataFlow
|
||||
import net.corda.core.utilities.UntrustworthyData
|
||||
|
||||
// Flow to start data vending without sending transaction. For testing only.
|
||||
class TestDataVendingFlow(otherSide: Party) : SendStateAndRefFlow(otherSide, emptyList()) {
|
||||
class TestDataVendingFlow(otherSideSession: FlowSession) : SendStateAndRefFlow(otherSideSession, emptyList()) {
|
||||
@Suspendable
|
||||
override fun sendPayloadAndReceiveDataRequest(otherSide: Party, payload: Any): UntrustworthyData<FetchDataFlow.Request> {
|
||||
override fun sendPayloadAndReceiveDataRequest(otherSideSession: FlowSession, payload: Any): UntrustworthyData<FetchDataFlow.Request> {
|
||||
return if (payload is List<*> && payload.isEmpty()) {
|
||||
// Hack to not send the first message.
|
||||
receive(otherSide)
|
||||
otherSideSession.receive()
|
||||
} else {
|
||||
super.sendPayloadAndReceiveDataRequest(otherSide, payload)
|
||||
super.sendPayloadAndReceiveDataRequest(this.otherSideSession, payload)
|
||||
}
|
||||
}
|
||||
}
|
@ -2,10 +2,7 @@ package net.corda.core.internal
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.InitiatedBy
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.TestDataVendingFlow
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
@ -195,20 +192,22 @@ class ResolveTransactionsFlowTest {
|
||||
// DOCEND 2
|
||||
|
||||
@InitiatingFlow
|
||||
private class TestFlow(private val resolveTransactionsFlow: ResolveTransactionsFlow, private val txCountLimit: Int? = null) : FlowLogic<List<SignedTransaction>>() {
|
||||
constructor(txHashes: Set<SecureHash>, otherSide: Party, txCountLimit: Int? = null) : this(ResolveTransactionsFlow(txHashes, otherSide), txCountLimit = txCountLimit)
|
||||
constructor(stx: SignedTransaction, otherSide: Party) : this(ResolveTransactionsFlow(stx, otherSide))
|
||||
private class TestFlow(val otherSide: Party, private val resolveTransactionsFlowFactory: (FlowSession) -> ResolveTransactionsFlow, private val txCountLimit: Int? = null) : FlowLogic<List<SignedTransaction>>() {
|
||||
constructor(txHashes: Set<SecureHash>, otherSide: Party, txCountLimit: Int? = null) : this(otherSide, { ResolveTransactionsFlow(txHashes, it) }, txCountLimit = txCountLimit)
|
||||
constructor(stx: SignedTransaction, otherSide: Party) : this(otherSide, { ResolveTransactionsFlow(stx, it) })
|
||||
|
||||
@Suspendable
|
||||
override fun call(): List<SignedTransaction> {
|
||||
val session = initiateFlow(otherSide)
|
||||
val resolveTransactionsFlow = resolveTransactionsFlowFactory(session)
|
||||
txCountLimit?.let { resolveTransactionsFlow.transactionCountLimit = it }
|
||||
return subFlow(resolveTransactionsFlow)
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(TestFlow::class)
|
||||
private class TestResponseFlow(val otherSide: Party) : FlowLogic<Void?>() {
|
||||
private class TestResponseFlow(val otherSideSession: FlowSession) : FlowLogic<Void?>() {
|
||||
@Suspendable
|
||||
override fun call() = subFlow(TestDataVendingFlow(otherSide))
|
||||
override fun call() = subFlow(TestDataVendingFlow(otherSideSession))
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.TestDataVendingFlow
|
||||
import net.corda.core.identity.Party
|
||||
@ -82,14 +83,14 @@ class AttachmentSerializationTest {
|
||||
mockNet.stopNodes()
|
||||
}
|
||||
|
||||
private class ServerLogic(private val client: Party, private val sendData: Boolean) : FlowLogic<Unit>() {
|
||||
private class ServerLogic(private val clientSession: FlowSession, private val sendData: Boolean) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
if (sendData) {
|
||||
subFlow(TestDataVendingFlow(client))
|
||||
subFlow(TestDataVendingFlow(clientSession))
|
||||
}
|
||||
receive<String>(client).unwrap { assertEquals("ping one", it) }
|
||||
sendAndReceive<String>(client, "pong").unwrap { assertEquals("ping two", it) }
|
||||
clientSession.receive<String>().unwrap { assertEquals("ping one", it) }
|
||||
clientSession.sendAndReceive<String>("pong").unwrap { assertEquals("ping two", it) }
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,9 +101,9 @@ class AttachmentSerializationTest {
|
||||
internal val server = server.info.chooseIdentity()
|
||||
|
||||
@Suspendable
|
||||
internal fun communicate() {
|
||||
sendAndReceive<String>(server, "ping one").unwrap { assertEquals("pong", it) }
|
||||
send(server, "ping two")
|
||||
internal fun communicate(serverSession: FlowSession) {
|
||||
serverSession.sendAndReceive<String>("ping one").unwrap { assertEquals("pong", it) }
|
||||
serverSession.send("ping two")
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
@ -121,7 +122,8 @@ class AttachmentSerializationTest {
|
||||
@Suspendable
|
||||
override fun getAttachmentContent(): String {
|
||||
val customAttachment = CustomAttachment(attachmentId, customContent)
|
||||
communicate()
|
||||
val session = initiateFlow(server)
|
||||
communicate(session)
|
||||
return customAttachment.customContent
|
||||
}
|
||||
}
|
||||
@ -130,7 +132,8 @@ class AttachmentSerializationTest {
|
||||
@Suspendable
|
||||
override fun getAttachmentContent(): String {
|
||||
val localAttachment = serviceHub.attachments.openAttachment(attachmentId)!!
|
||||
communicate()
|
||||
val session = initiateFlow(server)
|
||||
communicate(session)
|
||||
return localAttachment.extractContent()
|
||||
}
|
||||
}
|
||||
@ -138,9 +141,10 @@ class AttachmentSerializationTest {
|
||||
private class FetchAttachmentLogic(server: StartedNode<*>, private val attachmentId: SecureHash) : ClientLogic(server) {
|
||||
@Suspendable
|
||||
override fun getAttachmentContent(): String {
|
||||
val (downloadedAttachment) = subFlow(FetchAttachmentsFlow(setOf(attachmentId), server)).downloaded
|
||||
send(server, FetchDataFlow.Request.End)
|
||||
communicate()
|
||||
val serverSession = initiateFlow(server)
|
||||
val (downloadedAttachment) = subFlow(FetchAttachmentsFlow(setOf(attachmentId), serverSession)).downloaded
|
||||
serverSession.send(FetchDataFlow.Request.End)
|
||||
communicate(serverSession)
|
||||
return downloadedAttachment.extractContent()
|
||||
}
|
||||
}
|
||||
@ -148,7 +152,7 @@ class AttachmentSerializationTest {
|
||||
private fun launchFlow(clientLogic: ClientLogic, rounds: Int, sendData: Boolean = false) {
|
||||
server.internals.internalRegisterFlowFactory(
|
||||
ClientLogic::class.java,
|
||||
InitiatedFlowFactory.Core { ServerLogic(it.counterparty, sendData) },
|
||||
InitiatedFlowFactory.Core { ServerLogic(it, sendData) },
|
||||
ServerLogic::class.java,
|
||||
track = false)
|
||||
client.services.startFlow(clientLogic)
|
||||
|
@ -23,7 +23,7 @@ class IntegrationTestingTutorial {
|
||||
@Test
|
||||
fun `alice bob cash exchange example`() {
|
||||
// START 1
|
||||
driver {
|
||||
driver(extraCordappPackagesToScan = listOf("net.corda.finance")) {
|
||||
val aliceUser = User("aliceUser", "testPassword1", permissions = setOf(
|
||||
startFlowPermission<CashIssueFlow>(),
|
||||
startFlowPermission<CashPaymentFlow>()
|
||||
|
@ -2,7 +2,6 @@ package net.corda.docs;
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import net.corda.core.contracts.*;
|
||||
import net.corda.core.crypto.SecureHash;
|
||||
import net.corda.core.crypto.TransactionSignature;
|
||||
@ -29,6 +28,7 @@ import java.security.GeneralSecurityException;
|
||||
import java.security.PublicKey;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@ -37,6 +37,7 @@ import static net.corda.testing.TestConstants.getALICE_KEY;
|
||||
|
||||
// We group our two flows inside a singleton object to indicate that they work
|
||||
// together.
|
||||
@SuppressWarnings("unused")
|
||||
public class FlowCookbookJava {
|
||||
// ``InitiatorFlow`` is our first flow, and will communicate with
|
||||
// ``ResponderFlow``, below.
|
||||
@ -80,14 +81,14 @@ public class FlowCookbookJava {
|
||||
// subflow's progress steps in our flow's progress tracker.
|
||||
@Override
|
||||
public ProgressTracker childProgressTracker() {
|
||||
return CollectSignaturesFlow.Companion.tracker();
|
||||
return CollectSignaturesFlow.tracker();
|
||||
}
|
||||
};
|
||||
private static final Step VERIFYING_SIGS = new Step("Verifying a transaction's signatures.");
|
||||
private static final Step FINALISATION = new Step("Finalising a transaction.") {
|
||||
@Override
|
||||
public ProgressTracker childProgressTracker() {
|
||||
return FinalityFlow.Companion.tracker();
|
||||
return FinalityFlow.tracker();
|
||||
}
|
||||
};
|
||||
|
||||
@ -154,7 +155,8 @@ public class FlowCookbookJava {
|
||||
// registered to respond to this flow, and has a corresponding
|
||||
// ``receive`` call.
|
||||
// DOCSTART 4
|
||||
send(counterparty, new Object());
|
||||
FlowSession counterpartySession = initiateFlow(counterparty);
|
||||
counterpartySession.send(new Object());
|
||||
// DOCEND 4
|
||||
|
||||
// We can wait to receive arbitrary data of a specific type from a
|
||||
@ -177,7 +179,7 @@ public class FlowCookbookJava {
|
||||
// be what it appears to be! We must unwrap the
|
||||
// ``UntrustworthyData`` using a lambda.
|
||||
// DOCSTART 5
|
||||
UntrustworthyData<Integer> packet1 = receive(Integer.class, counterparty);
|
||||
UntrustworthyData<Integer> packet1 = counterpartySession.receive(Integer.class);
|
||||
Integer integer = packet1.unwrap(data -> {
|
||||
// Perform checking on the object received.
|
||||
// T O D O: Check the received object.
|
||||
@ -191,7 +193,7 @@ public class FlowCookbookJava {
|
||||
// data sent doesn't need to match the type of the data received
|
||||
// back.
|
||||
// DOCSTART 7
|
||||
UntrustworthyData<Boolean> packet2 = sendAndReceive(Boolean.class, counterparty, "You can send and receive any class!");
|
||||
UntrustworthyData<Boolean> packet2 = counterpartySession.sendAndReceive(Boolean.class, "You can send and receive any class!");
|
||||
Boolean bool = packet2.unwrap(data -> {
|
||||
// Perform checking on the object received.
|
||||
// T O D O: Check the received object.
|
||||
@ -204,8 +206,9 @@ public class FlowCookbookJava {
|
||||
// counterparty. A flow can send messages to as many parties as it
|
||||
// likes, and each party can invoke a different response flow.
|
||||
// DOCSTART 6
|
||||
send(regulator, new Object());
|
||||
UntrustworthyData<Object> packet3 = receive(Object.class, regulator);
|
||||
FlowSession regulatorSession = initiateFlow(regulator);
|
||||
regulatorSession.send(new Object());
|
||||
UntrustworthyData<Object> packet3 = regulatorSession.receive(Object.class);
|
||||
// DOCEND 6
|
||||
|
||||
/*------------------------------------
|
||||
@ -395,10 +398,10 @@ public class FlowCookbookJava {
|
||||
// for data request until the transaction is resolved and verified
|
||||
// on the other side:
|
||||
// DOCSTART 12
|
||||
subFlow(new SendTransactionFlow(counterparty, twiceSignedTx));
|
||||
subFlow(new SendTransactionFlow(counterpartySession, twiceSignedTx));
|
||||
|
||||
// Optional request verification to further restrict data access.
|
||||
subFlow(new SendTransactionFlow(counterparty, twiceSignedTx) {
|
||||
subFlow(new SendTransactionFlow(counterpartySession, twiceSignedTx) {
|
||||
@Override
|
||||
protected void verifyDataRequest(@NotNull FetchDataFlow.Request.Data dataRequest) {
|
||||
// Extra request verification.
|
||||
@ -408,17 +411,17 @@ public class FlowCookbookJava {
|
||||
|
||||
// We can receive the transaction using ``ReceiveTransactionFlow``,
|
||||
// which will automatically download all the dependencies and verify
|
||||
// the transaction
|
||||
// the transaction and then record in our vault
|
||||
// DOCSTART 13
|
||||
SignedTransaction verifiedTransaction = subFlow(new ReceiveTransactionFlow(counterparty));
|
||||
SignedTransaction verifiedTransaction = subFlow(new ReceiveTransactionFlow(counterpartySession));
|
||||
// DOCEND 13
|
||||
|
||||
// We can also send and receive a `StateAndRef` dependency chain and automatically resolve its dependencies.
|
||||
// DOCSTART 14
|
||||
subFlow(new SendStateAndRefFlow(counterparty, dummyStates));
|
||||
subFlow(new SendStateAndRefFlow(counterpartySession, dummyStates));
|
||||
|
||||
// On the receive side ...
|
||||
List<StateAndRef<DummyState>> resolvedStateAndRef = subFlow(new ReceiveStateAndRefFlow<DummyState>(counterparty));
|
||||
List<StateAndRef<DummyState>> resolvedStateAndRef = subFlow(new ReceiveStateAndRefFlow<DummyState>(counterpartySession));
|
||||
// DOCEND 14
|
||||
|
||||
try {
|
||||
@ -475,7 +478,7 @@ public class FlowCookbookJava {
|
||||
// other required signers using ``CollectSignaturesFlow``.
|
||||
// The responder flow will need to call ``SignTransactionFlow``.
|
||||
// DOCSTART 15
|
||||
SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow(twiceSignedTx, SIGS_GATHERING.childProgressTracker()));
|
||||
SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow(twiceSignedTx, Collections.emptySet(), SIGS_GATHERING.childProgressTracker()));
|
||||
// DOCEND 15
|
||||
|
||||
/*------------------------
|
||||
@ -517,13 +520,13 @@ public class FlowCookbookJava {
|
||||
// We notarise the transaction and get it recorded in the vault of
|
||||
// the participants of all the transaction's states.
|
||||
// DOCSTART 9
|
||||
SignedTransaction notarisedTx1 = subFlow(new FinalityFlow(fullySignedTx, FINALISATION.childProgressTracker())).get(0);
|
||||
SignedTransaction notarisedTx1 = subFlow(new FinalityFlow(fullySignedTx, FINALISATION.childProgressTracker()));
|
||||
// DOCEND 9
|
||||
// We can also choose to send it to additional parties who aren't one
|
||||
// of the state's participants.
|
||||
// DOCSTART 10
|
||||
Set<Party> additionalParties = ImmutableSet.of(regulator);
|
||||
SignedTransaction notarisedTx2 = subFlow(new FinalityFlow(ImmutableList.of(fullySignedTx), additionalParties, FINALISATION.childProgressTracker())).get(0);
|
||||
Set<Party> additionalParties = Collections.singleton(regulator);
|
||||
SignedTransaction notarisedTx2 = subFlow(new FinalityFlow(fullySignedTx, additionalParties, FINALISATION.childProgressTracker()));
|
||||
// DOCEND 10
|
||||
|
||||
return null;
|
||||
@ -540,10 +543,10 @@ public class FlowCookbookJava {
|
||||
@InitiatedBy(InitiatorFlow.class)
|
||||
public static class ResponderFlow extends FlowLogic<Void> {
|
||||
|
||||
private final Party counterparty;
|
||||
private final FlowSession counterpartySession;
|
||||
|
||||
public ResponderFlow(Party counterparty) {
|
||||
this.counterparty = counterparty;
|
||||
public ResponderFlow(FlowSession counterpartySession) {
|
||||
this.counterpartySession = counterpartySession;
|
||||
}
|
||||
|
||||
private static final Step RECEIVING_AND_SENDING_DATA = new Step("Sending data between parties.");
|
||||
@ -575,9 +578,9 @@ public class FlowCookbookJava {
|
||||
// ``Boolean`` instance back
|
||||
// Our side of the flow must mirror these calls.
|
||||
// DOCSTART 8
|
||||
Object obj = receive(Object.class, counterparty).unwrap(data -> data);
|
||||
String string = sendAndReceive(String.class, counterparty, 99).unwrap(data -> data);
|
||||
send(counterparty, true);
|
||||
Object obj = counterpartySession.receive(Object.class).unwrap(data -> data);
|
||||
String string = counterpartySession.sendAndReceive(String.class, 99).unwrap(data -> data);
|
||||
counterpartySession.send(true);
|
||||
// DOCEND 8
|
||||
|
||||
/*-----------------------------------------
|
||||
@ -590,8 +593,8 @@ public class FlowCookbookJava {
|
||||
// ``SignTransactionFlow`` subclass.
|
||||
// DOCSTART 16
|
||||
class SignTxFlow extends SignTransactionFlow {
|
||||
private SignTxFlow(Party otherParty, ProgressTracker progressTracker) {
|
||||
super(otherParty, progressTracker);
|
||||
private SignTxFlow(FlowSession otherSession, ProgressTracker progressTracker) {
|
||||
super(otherSession, progressTracker);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -605,7 +608,7 @@ public class FlowCookbookJava {
|
||||
}
|
||||
}
|
||||
|
||||
subFlow(new SignTxFlow(counterparty, SignTransactionFlow.Companion.tracker()));
|
||||
subFlow(new SignTxFlow(counterpartySession, SignTransactionFlow.tracker()));
|
||||
// DOCEND 16
|
||||
|
||||
/*------------------------------
|
||||
|
@ -3,7 +3,6 @@ package net.corda.docs
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.TransactionVerificationException
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.services.CordaService
|
||||
import net.corda.core.node.services.TimeWindowChecker
|
||||
@ -19,7 +18,7 @@ class MyCustomValidatingNotaryService(override val services: ServiceHub, overrid
|
||||
override val timeWindowChecker = TimeWindowChecker(services.clock)
|
||||
override val uniquenessProvider = PersistentUniquenessProvider()
|
||||
|
||||
override fun createServiceFlow(otherParty: Party): FlowLogic<Void?> = MyValidatingNotaryFlow(otherParty, this)
|
||||
override fun createServiceFlow(otherPartySession: FlowSession): FlowLogic<Void?> = MyValidatingNotaryFlow(otherPartySession, this)
|
||||
|
||||
override fun start() {}
|
||||
override fun stop() {}
|
||||
@ -27,7 +26,7 @@ class MyCustomValidatingNotaryService(override val services: ServiceHub, overrid
|
||||
// END 1
|
||||
|
||||
// START 2
|
||||
class MyValidatingNotaryFlow(otherSide: Party, service: MyCustomValidatingNotaryService) : NotaryFlow.Service(otherSide, service) {
|
||||
class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidatingNotaryService) : NotaryFlow.Service(otherSide, service) {
|
||||
/**
|
||||
* The received transaction is checked for contract-validity, for which the caller also has to to reveal the whole
|
||||
* transaction dependency chain.
|
||||
@ -35,7 +34,7 @@ class MyValidatingNotaryFlow(otherSide: Party, service: MyCustomValidatingNotary
|
||||
@Suspendable
|
||||
override fun receiveAndVerifyTx(): TransactionParts {
|
||||
try {
|
||||
val stx = subFlow(ReceiveTransactionFlow(otherSide, checkSufficientSignatures = false))
|
||||
val stx = subFlow(ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = false))
|
||||
checkNotary(stx.notary)
|
||||
checkSignatures(stx)
|
||||
val wtx = stx.tx
|
||||
|
@ -2,10 +2,7 @@ package net.corda.docs
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.InitiatedBy
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.services.CordaService
|
||||
@ -85,12 +82,12 @@ object TopupIssuerFlow {
|
||||
@Throws(CashException::class)
|
||||
override fun call(): List<AbstractCashFlow.Result> {
|
||||
val topupRequest = TopupRequest(issueToParty, issueToPartyRef, notaryParty)
|
||||
return sendAndReceive<List<AbstractCashFlow.Result>>(issuerBankParty, topupRequest).unwrap { it }
|
||||
return initiateFlow(issuerBankParty).sendAndReceive<List<AbstractCashFlow.Result>>(topupRequest).unwrap { it }
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(TopupIssuanceRequester::class)
|
||||
class TopupIssuer(val otherParty: Party) : FlowLogic<List<SignedTransaction>>() {
|
||||
class TopupIssuer(val otherPartySession: FlowSession) : FlowLogic<List<SignedTransaction>>() {
|
||||
companion object {
|
||||
object AWAITING_REQUEST : ProgressTracker.Step("Awaiting issuance request")
|
||||
object ISSUING : ProgressTracker.Step("Issuing asset")
|
||||
@ -107,7 +104,7 @@ object TopupIssuerFlow {
|
||||
@Throws(CashException::class)
|
||||
override fun call(): List<SignedTransaction> {
|
||||
progressTracker.currentStep = AWAITING_REQUEST
|
||||
val topupRequest = receive<TopupRequest>(otherParty).unwrap {
|
||||
val topupRequest = otherPartySession.receive<TopupRequest>().unwrap {
|
||||
it
|
||||
}
|
||||
|
||||
@ -122,7 +119,7 @@ object TopupIssuerFlow {
|
||||
return@map txn.stx
|
||||
}
|
||||
|
||||
send(otherParty, txns)
|
||||
otherPartySession.send(txns)
|
||||
return txns
|
||||
}
|
||||
// DOCEND TopupIssuer
|
||||
|
@ -16,8 +16,11 @@ import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.*
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.ProgressTracker.Step
|
||||
import net.corda.core.utilities.UntrustworthyData
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.testing.ALICE_PUBKEY
|
||||
import net.corda.testing.contracts.DUMMY_PROGRAM_ID
|
||||
@ -39,7 +42,7 @@ object FlowCookbook {
|
||||
@StartableByRPC
|
||||
// Every flow must subclass ``FlowLogic``. The generic indicates the
|
||||
// flow's return type.
|
||||
class InitiatorFlow(val arg1: Boolean, val arg2: Int, val counterparty: Party, val regulator: Party) : FlowLogic<Unit>() {
|
||||
class InitiatorFlow(val arg1: Boolean, val arg2: Int, private val counterparty: Party, val regulator: Party) : FlowLogic<Unit>() {
|
||||
|
||||
/**---------------------------------
|
||||
* WIRING UP THE PROGRESS TRACKER *
|
||||
@ -135,7 +138,8 @@ object FlowCookbook {
|
||||
// registered to respond to this flow, and has a corresponding
|
||||
// ``receive`` call.
|
||||
// DOCSTART 4
|
||||
send(counterparty, Any())
|
||||
val counterpartySession = initiateFlow(counterparty)
|
||||
counterpartySession.send(Any())
|
||||
// DOCEND 4
|
||||
|
||||
// We can wait to receive arbitrary data of a specific type from a
|
||||
@ -158,7 +162,7 @@ object FlowCookbook {
|
||||
// be what it appears to be! We must unwrap the
|
||||
// ``UntrustworthyData`` using a lambda.
|
||||
// DOCSTART 5
|
||||
val packet1: UntrustworthyData<Int> = receive<Int>(counterparty)
|
||||
val packet1: UntrustworthyData<Int> = counterpartySession.receive<Int>()
|
||||
val int: Int = packet1.unwrap { data ->
|
||||
// Perform checking on the object received.
|
||||
// T O D O: Check the received object.
|
||||
@ -172,7 +176,7 @@ object FlowCookbook {
|
||||
// data sent doesn't need to match the type of the data received
|
||||
// back.
|
||||
// DOCSTART 7
|
||||
val packet2: UntrustworthyData<Boolean> = sendAndReceive<Boolean>(counterparty, "You can send and receive any class!")
|
||||
val packet2: UntrustworthyData<Boolean> = counterpartySession.sendAndReceive<Boolean>("You can send and receive any class!")
|
||||
val boolean: Boolean = packet2.unwrap { data ->
|
||||
// Perform checking on the object received.
|
||||
// T O D O: Check the received object.
|
||||
@ -185,8 +189,9 @@ object FlowCookbook {
|
||||
// counterparty. A flow can send messages to as many parties as it
|
||||
// likes, and each party can invoke a different response flow.
|
||||
// DOCSTART 6
|
||||
send(regulator, Any())
|
||||
val packet3: UntrustworthyData<Any> = receive<Any>(regulator)
|
||||
val regulatorSession = initiateFlow(regulator)
|
||||
regulatorSession.send(Any())
|
||||
val packet3: UntrustworthyData<Any> = regulatorSession.receive<Any>()
|
||||
// DOCEND 6
|
||||
|
||||
/**-----------------------------------
|
||||
@ -378,10 +383,10 @@ object FlowCookbook {
|
||||
// for data request until the transaction is resolved and verified
|
||||
// on the other side:
|
||||
// DOCSTART 12
|
||||
subFlow(SendTransactionFlow(counterparty, twiceSignedTx))
|
||||
subFlow(SendTransactionFlow(counterpartySession, twiceSignedTx))
|
||||
|
||||
// Optional request verification to further restrict data access.
|
||||
subFlow(object : SendTransactionFlow(counterparty, twiceSignedTx) {
|
||||
subFlow(object : SendTransactionFlow(counterpartySession, twiceSignedTx) {
|
||||
override fun verifyDataRequest(dataRequest: FetchDataFlow.Request.Data) {
|
||||
// Extra request verification.
|
||||
}
|
||||
@ -392,16 +397,16 @@ object FlowCookbook {
|
||||
// which will automatically download all the dependencies and verify
|
||||
// the transaction
|
||||
// DOCSTART 13
|
||||
val verifiedTransaction = subFlow(ReceiveTransactionFlow(counterparty))
|
||||
val verifiedTransaction = subFlow(ReceiveTransactionFlow(counterpartySession))
|
||||
// DOCEND 13
|
||||
|
||||
// We can also send and receive a `StateAndRef` dependency chain
|
||||
// and automatically resolve its dependencies.
|
||||
// DOCSTART 14
|
||||
subFlow(SendStateAndRefFlow(counterparty, dummyStates))
|
||||
subFlow(SendStateAndRefFlow(counterpartySession, dummyStates))
|
||||
|
||||
// On the receive side ...
|
||||
val resolvedStateAndRef = subFlow(ReceiveStateAndRefFlow<DummyState>(counterparty))
|
||||
val resolvedStateAndRef = subFlow(ReceiveStateAndRefFlow<DummyState>(counterpartySession))
|
||||
// DOCEND 14
|
||||
|
||||
// We can now verify the transaction to ensure that it satisfies
|
||||
@ -452,7 +457,7 @@ object FlowCookbook {
|
||||
// other required signers using ``CollectSignaturesFlow``.
|
||||
// The responder flow will need to call ``SignTransactionFlow``.
|
||||
// DOCSTART 15
|
||||
val fullySignedTx: SignedTransaction = subFlow(CollectSignaturesFlow(twiceSignedTx, SIGS_GATHERING.childProgressTracker()))
|
||||
val fullySignedTx: SignedTransaction = subFlow(CollectSignaturesFlow(twiceSignedTx, emptySet(), SIGS_GATHERING.childProgressTracker()))
|
||||
// DOCEND 15
|
||||
|
||||
/**-----------------------
|
||||
@ -488,13 +493,13 @@ object FlowCookbook {
|
||||
// We notarise the transaction and get it recorded in the vault of
|
||||
// the participants of all the transaction's states.
|
||||
// DOCSTART 9
|
||||
val notarisedTx1: SignedTransaction = subFlow(FinalityFlow(fullySignedTx, FINALISATION.childProgressTracker())).single()
|
||||
val notarisedTx1: SignedTransaction = subFlow(FinalityFlow(fullySignedTx, FINALISATION.childProgressTracker()))
|
||||
// DOCEND 9
|
||||
// We can also choose to send it to additional parties who aren't one
|
||||
// of the state's participants.
|
||||
// DOCSTART 10
|
||||
val additionalParties: Set<Party> = setOf(regulator)
|
||||
val notarisedTx2: SignedTransaction = subFlow(FinalityFlow(listOf(fullySignedTx), additionalParties, FINALISATION.childProgressTracker())).single()
|
||||
val notarisedTx2: SignedTransaction = subFlow(FinalityFlow(fullySignedTx, additionalParties, FINALISATION.childProgressTracker()))
|
||||
// DOCEND 10
|
||||
}
|
||||
}
|
||||
@ -507,7 +512,7 @@ object FlowCookbook {
|
||||
// Each node also has several flow pairs registered by default - see
|
||||
// ``AbstractNode.installCoreFlows``.
|
||||
@InitiatedBy(InitiatorFlow::class)
|
||||
class ResponderFlow(val counterparty: Party) : FlowLogic<Unit>() {
|
||||
class ResponderFlow(val counterpartySession: FlowSession) : FlowLogic<Unit>() {
|
||||
|
||||
companion object {
|
||||
object RECEIVING_AND_SENDING_DATA : Step("Sending data between parties.")
|
||||
@ -541,9 +546,9 @@ object FlowCookbook {
|
||||
// ``Boolean`` instance back
|
||||
// Our side of the flow must mirror these calls.
|
||||
// DOCSTART 8
|
||||
val any: Any = receive<Any>(counterparty).unwrap { data -> data }
|
||||
val string: String = sendAndReceive<String>(counterparty, 99).unwrap { data -> data }
|
||||
send(counterparty, true)
|
||||
val any: Any = counterpartySession.receive<Any>().unwrap { data -> data }
|
||||
val string: String = counterpartySession.sendAndReceive<String>(99).unwrap { data -> data }
|
||||
counterpartySession.send(true)
|
||||
// DOCEND 8
|
||||
|
||||
/**----------------------------------------
|
||||
@ -555,7 +560,7 @@ object FlowCookbook {
|
||||
// ``CollectSignaturesFlow``. It does so my invoking its own
|
||||
// ``SignTransactionFlow`` subclass.
|
||||
// DOCSTART 16
|
||||
val signTransactionFlow: SignTransactionFlow = object : SignTransactionFlow(counterparty) {
|
||||
val signTransactionFlow: SignTransactionFlow = object : SignTransactionFlow(counterpartySession) {
|
||||
override fun checkTransaction(stx: SignedTransaction) = requireThat {
|
||||
// Any additional checking we see fit...
|
||||
val outputState = stx.tx.outputsOfType<DummyState>().single()
|
||||
|
@ -50,7 +50,7 @@ private fun gatherOurInputs(serviceHub: ServiceHub,
|
||||
val eligibleStates = serviceHub.vaultService.tryLockFungibleStatesForSpending(lockId, fullCriteria, amountRequired.withoutIssuer(), Cash.State::class.java)
|
||||
|
||||
check(eligibleStates.isNotEmpty()) { "Insufficient funds" }
|
||||
val amount = eligibleStates.fold(0L) { tot, x -> tot + x.state.data.amount.quantity }
|
||||
val amount = eligibleStates.fold(0L) { tot, (state) -> tot + state.data.amount.quantity }
|
||||
val change = amount - amountRequired.quantity
|
||||
|
||||
return Pair(eligibleStates, change)
|
||||
@ -87,25 +87,25 @@ private fun prepareOurInputsAndOutputs(serviceHub: ServiceHub, lockId: UUID, req
|
||||
// A flow representing creating a transaction that
|
||||
// carries out exchange of cash assets.
|
||||
@InitiatingFlow
|
||||
class ForeignExchangeFlow(val tradeId: String,
|
||||
val baseCurrencyAmount: Amount<Issued<Currency>>,
|
||||
val quoteCurrencyAmount: Amount<Issued<Currency>>,
|
||||
val baseCurrencyBuyer: Party,
|
||||
val baseCurrencySeller: Party) : FlowLogic<SecureHash>() {
|
||||
class ForeignExchangeFlow(private val tradeId: String,
|
||||
private val baseCurrencyAmount: Amount<Issued<Currency>>,
|
||||
private val quoteCurrencyAmount: Amount<Issued<Currency>>,
|
||||
private val counterparty: Party,
|
||||
private val weAreBaseCurrencySeller: Boolean) : FlowLogic<SecureHash>() {
|
||||
@Suspendable
|
||||
override fun call(): SecureHash {
|
||||
// Select correct sides of the Fx exchange to query for.
|
||||
// Specifically we own the assets we wish to sell.
|
||||
// Also prepare the other side query
|
||||
val (localRequest, remoteRequest) = if (serviceHub.myInfo.isLegalIdentity(baseCurrencySeller)) {
|
||||
val local = FxRequest(tradeId, baseCurrencyAmount, baseCurrencySeller, baseCurrencyBuyer)
|
||||
val remote = FxRequest(tradeId, quoteCurrencyAmount, baseCurrencyBuyer, baseCurrencySeller)
|
||||
val (localRequest, remoteRequest) = if (weAreBaseCurrencySeller) {
|
||||
val local = FxRequest(tradeId, baseCurrencyAmount, ourIdentity, counterparty)
|
||||
val remote = FxRequest(tradeId, quoteCurrencyAmount, counterparty, ourIdentity)
|
||||
Pair(local, remote)
|
||||
} else if (serviceHub.myInfo.isLegalIdentity(baseCurrencyBuyer)) {
|
||||
val local = FxRequest(tradeId, quoteCurrencyAmount, baseCurrencyBuyer, baseCurrencySeller)
|
||||
val remote = FxRequest(tradeId, baseCurrencyAmount, baseCurrencySeller, baseCurrencyBuyer)
|
||||
} else {
|
||||
val local = FxRequest(tradeId, quoteCurrencyAmount, ourIdentity, counterparty)
|
||||
val remote = FxRequest(tradeId, baseCurrencyAmount, counterparty, ourIdentity)
|
||||
Pair(local, remote)
|
||||
} else throw IllegalArgumentException("Our identity must be one of the parties in the trade.")
|
||||
}
|
||||
|
||||
// Call the helper method to identify suitable inputs and make the outputs
|
||||
val (ourInputStates, ourOutputStates) = prepareOurInputsAndOutputs(serviceHub, runId.uuid, localRequest)
|
||||
@ -117,9 +117,10 @@ class ForeignExchangeFlow(val tradeId: String,
|
||||
|
||||
// Send the request to the counterparty to verify and call their version of prepareOurInputsAndOutputs
|
||||
// Then they can return their candidate states
|
||||
send(remoteRequestWithNotary.owner, remoteRequestWithNotary)
|
||||
val theirInputStates = subFlow(ReceiveStateAndRefFlow<Cash.State>(remoteRequestWithNotary.owner))
|
||||
val theirOutputStates = receive<List<Cash.State>>(remoteRequestWithNotary.owner).unwrap {
|
||||
val counterpartySession = initiateFlow(counterparty)
|
||||
counterpartySession.send(remoteRequestWithNotary)
|
||||
val theirInputStates = subFlow(ReceiveStateAndRefFlow<Cash.State>(counterpartySession))
|
||||
val theirOutputStates = counterpartySession.receive<List<Cash.State>>().unwrap {
|
||||
require(theirInputStates.all { it.state.notary == notary }) {
|
||||
"notary of remote states must be same as for our states"
|
||||
}
|
||||
@ -144,9 +145,9 @@ class ForeignExchangeFlow(val tradeId: String,
|
||||
val signedTransaction = buildTradeProposal(ourInputStates, ourOutputStates, theirInputStates, theirOutputStates)
|
||||
|
||||
// pass transaction details to the counterparty to revalidate and confirm with a signature
|
||||
// Allow otherParty to access our data to resolve the transaction.
|
||||
subFlow(SendTransactionFlow(remoteRequestWithNotary.owner, signedTransaction))
|
||||
val allPartySignedTx = receive<TransactionSignature>(remoteRequestWithNotary.owner).unwrap {
|
||||
// Allow counterparty to access our data to resolve the transaction.
|
||||
subFlow(SendTransactionFlow(counterpartySession, signedTransaction))
|
||||
val allPartySignedTx = counterpartySession.receive<TransactionSignature>().unwrap {
|
||||
val withNewSignature = signedTransaction + it
|
||||
// check all signatures are present except the notary
|
||||
withNewSignature.verifySignaturesExcept(withNewSignature.tx.notary!!.owningKey)
|
||||
@ -160,7 +161,7 @@ class ForeignExchangeFlow(val tradeId: String,
|
||||
}
|
||||
|
||||
// Initiate the standard protocol to notarise and distribute to the involved parties.
|
||||
subFlow(FinalityFlow(allPartySignedTx, setOf(baseCurrencyBuyer, baseCurrencySeller)))
|
||||
subFlow(FinalityFlow(allPartySignedTx, setOf(counterparty)))
|
||||
|
||||
return allPartySignedTx.id
|
||||
}
|
||||
@ -195,11 +196,11 @@ class ForeignExchangeFlow(val tradeId: String,
|
||||
}
|
||||
|
||||
@InitiatedBy(ForeignExchangeFlow::class)
|
||||
class ForeignExchangeRemoteFlow(private val source: Party) : FlowLogic<Unit>() {
|
||||
class ForeignExchangeRemoteFlow(private val source: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
// Initial receive from remote party
|
||||
val request = receive<FxRequest>(source).unwrap {
|
||||
val request = source.receive<FxRequest>().unwrap {
|
||||
// We would need to check that this is a known trade ID here!
|
||||
// Also that the amounts and source are correct with the trade details.
|
||||
// In a production system there would be other Corda contracts tracking
|
||||
@ -209,7 +210,7 @@ class ForeignExchangeRemoteFlow(private val source: Party) : FlowLogic<Unit>() {
|
||||
require(serviceHub.myInfo.isLegalIdentity(it.owner)) {
|
||||
"Request does not include the correct counterparty"
|
||||
}
|
||||
require(source == it.counterparty) {
|
||||
require(source.counterparty == it.counterparty) {
|
||||
"Request does not include the correct counterparty"
|
||||
}
|
||||
it // return validated request
|
||||
@ -224,18 +225,13 @@ class ForeignExchangeRemoteFlow(private val source: Party) : FlowLogic<Unit>() {
|
||||
|
||||
// Send back our proposed states and await the full transaction to verify
|
||||
val ourKey = serviceHub.keyManagementService.filterMyKeys(ourInputState.flatMap { it.state.data.participants }.map { it.owningKey }).single()
|
||||
// SendStateAndRefFlow allows otherParty to access our transaction data to resolve the transaction.
|
||||
// SendStateAndRefFlow allows counterparty to access our transaction data to resolve the transaction.
|
||||
subFlow(SendStateAndRefFlow(source, ourInputState))
|
||||
send(source, ourOutputState)
|
||||
source.send(ourOutputState)
|
||||
val proposedTrade = subFlow(ReceiveTransactionFlow(source, checkSufficientSignatures = false)).let {
|
||||
val wtx = it.tx
|
||||
// check all signatures are present except our own and the notary
|
||||
it.verifySignaturesExcept(ourKey, wtx.notary!!.owningKey)
|
||||
|
||||
// This verifies that the transaction is contract-valid, even though it is missing signatures.
|
||||
// In a full solution there would be states tracking the trade request which
|
||||
// would be included in the transaction and enforce the amounts and tradeId
|
||||
wtx.toLedgerTransaction(serviceHub).verify()
|
||||
it // return the SignedTransaction
|
||||
}
|
||||
|
||||
@ -243,7 +239,7 @@ class ForeignExchangeRemoteFlow(private val source: Party) : FlowLogic<Unit>() {
|
||||
val ourSignature = serviceHub.createSignature(proposedTrade, ourKey)
|
||||
|
||||
// send the other side our signature.
|
||||
send(source, ourSignature)
|
||||
source.send(ourSignature)
|
||||
// N.B. The FinalityProtocol will be responsible for Notarising the SignedTransaction
|
||||
// and broadcasting the result to us.
|
||||
}
|
||||
|
@ -3,10 +3,7 @@ package net.corda.docs
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.flows.FinalityFlow
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.InitiatedBy
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.services.queryBy
|
||||
@ -110,7 +107,7 @@ class SubmitTradeApprovalFlow(private val tradeId: String,
|
||||
// We can automatically sign as there is no untrusted data.
|
||||
val signedTx = serviceHub.signInitialTransaction(tx)
|
||||
// Notarise and distribute.
|
||||
subFlow(FinalityFlow(signedTx, setOf(ourIdentity, counterparty)))
|
||||
subFlow(FinalityFlow(signedTx, setOf(counterparty)))
|
||||
// Return the initial state
|
||||
return signedTx.tx.outRef(0)
|
||||
}
|
||||
@ -175,7 +172,8 @@ class SubmitCompletionFlow(private val ref: StateRef, private val verdict: Workf
|
||||
val selfSignedTx = serviceHub.signInitialTransaction(tx)
|
||||
//DOCEND 2
|
||||
// Send the signed transaction to the originator and await their signature to confirm
|
||||
val allPartySignedTx = sendAndReceive<TransactionSignature>(newState.source, selfSignedTx).unwrap {
|
||||
val session = initiateFlow(newState.source)
|
||||
val allPartySignedTx = session.sendAndReceive<TransactionSignature>(selfSignedTx).unwrap {
|
||||
// Add their signature to our unmodified transaction. To check they signed the same tx.
|
||||
val agreedTx = selfSignedTx + it
|
||||
// Receive back their signature and confirm that it is for an unmodified transaction
|
||||
@ -189,7 +187,7 @@ class SubmitCompletionFlow(private val ref: StateRef, private val verdict: Workf
|
||||
}
|
||||
// DOCSTART 4
|
||||
// Notarise and distribute the completed transaction.
|
||||
subFlow(FinalityFlow(allPartySignedTx, setOf(latestRecord.state.data.source, latestRecord.state.data.counterparty)))
|
||||
subFlow(FinalityFlow(allPartySignedTx, setOf(newState.source)))
|
||||
// DOCEND 4
|
||||
// Return back the details of the completed state/transaction.
|
||||
return allPartySignedTx.tx.outRef(0)
|
||||
@ -202,12 +200,12 @@ class SubmitCompletionFlow(private val ref: StateRef, private val verdict: Workf
|
||||
* transaction to the ledger.
|
||||
*/
|
||||
@InitiatedBy(SubmitCompletionFlow::class)
|
||||
class RecordCompletionFlow(private val source: Party) : FlowLogic<Unit>() {
|
||||
class RecordCompletionFlow(private val sourceSession: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
// DOCSTART 3
|
||||
// First we receive the verdict transaction signed by their single key
|
||||
val completeTx = receive<SignedTransaction>(source).unwrap {
|
||||
val completeTx = sourceSession.receive<SignedTransaction>().unwrap {
|
||||
// Check the transaction is signed apart from our own key and the notary
|
||||
it.verifySignaturesExcept(ourIdentity.owningKey, it.tx.notary!!.owningKey)
|
||||
// Check the transaction data is correctly formed
|
||||
@ -223,7 +221,7 @@ class RecordCompletionFlow(private val source: Party) : FlowLogic<Unit>() {
|
||||
require(serviceHub.myInfo.isLegalIdentity(state.state.data.source)) {
|
||||
"Proposal not one of our original proposals"
|
||||
}
|
||||
require(state.state.data.counterparty == source) {
|
||||
require(state.state.data.counterparty == sourceSession.counterparty) {
|
||||
"Proposal not for sent from correct source"
|
||||
}
|
||||
it
|
||||
@ -232,7 +230,7 @@ class RecordCompletionFlow(private val source: Party) : FlowLogic<Unit>() {
|
||||
// Having verified the SignedTransaction passed to us we can sign it too
|
||||
val ourSignature = serviceHub.createSignature(completeTx)
|
||||
// Send our signature to the other party.
|
||||
send(source, ourSignature)
|
||||
sourceSession.send(ourSignature)
|
||||
// N.B. The FinalityProtocol will be responsible for Notarising the SignedTransaction
|
||||
// and broadcasting the result to us.
|
||||
}
|
||||
|
@ -7,11 +7,11 @@ import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.finance.*
|
||||
import net.corda.finance.contracts.getCashBalances
|
||||
import net.corda.finance.flows.CashIssueFlow
|
||||
import net.corda.node.internal.StartedNode
|
||||
import net.corda.finance.schemas.CashSchemaV1
|
||||
import net.corda.nodeapi.ServiceInfo
|
||||
import net.corda.node.internal.StartedNode
|
||||
import net.corda.node.services.network.NetworkMapService
|
||||
import net.corda.node.services.transactions.ValidatingNotaryService
|
||||
import net.corda.nodeapi.ServiceInfo
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
import net.corda.testing.DUMMY_NOTARY_KEY
|
||||
import net.corda.testing.chooseIdentity
|
||||
@ -76,8 +76,8 @@ class FxTransactionBuildTutorialTest {
|
||||
val doIt = nodeA.services.startFlow(ForeignExchangeFlow("trade1",
|
||||
POUNDS(100).issuedBy(nodeB.info.chooseIdentity().ref(0x01)),
|
||||
DOLLARS(200).issuedBy(nodeA.info.chooseIdentity().ref(0x01)),
|
||||
nodeA.info.chooseIdentity(),
|
||||
nodeB.info.chooseIdentity()))
|
||||
nodeB.info.chooseIdentity(),
|
||||
weAreBaseCurrencySeller = false))
|
||||
// wait for the flow to finish and the vault updates to be done
|
||||
doIt.resultFuture.getOrThrow()
|
||||
// Get the balances when the vault updates
|
||||
|
@ -9,9 +9,9 @@ import net.corda.core.node.services.vault.QueryCriteria
|
||||
import net.corda.core.toFuture
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.node.internal.StartedNode
|
||||
import net.corda.nodeapi.ServiceInfo
|
||||
import net.corda.node.services.network.NetworkMapService
|
||||
import net.corda.node.services.transactions.ValidatingNotaryService
|
||||
import net.corda.nodeapi.ServiceInfo
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
import net.corda.testing.DUMMY_NOTARY_KEY
|
||||
import net.corda.testing.chooseIdentity
|
||||
|
@ -27,9 +27,9 @@ abstract class AbstractCashFlow<out T>(override val progressTracker: ProgressTra
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
protected fun finaliseTx(participants: Set<Party>, tx: SignedTransaction, message: String) {
|
||||
protected fun finaliseTx(tx: SignedTransaction, extraParticipants: Set<Party>, message: String): SignedTransaction {
|
||||
try {
|
||||
subFlow(FinalityFlow(tx, participants))
|
||||
return subFlow(FinalityFlow(tx, extraParticipants))
|
||||
} catch (e: NotaryException) {
|
||||
throw CashException(message, e)
|
||||
}
|
||||
|
@ -44,9 +44,10 @@ class CashExitFlow(private val amount: Amount<Currency>,
|
||||
@Throws(CashException::class)
|
||||
override fun call(): AbstractCashFlow.Result {
|
||||
progressTracker.currentStep = GENERATING_TX
|
||||
val builder = TransactionBuilder(notary = null as Party?)
|
||||
val builder = TransactionBuilder(notary = null)
|
||||
val issuer = ourIdentity.ref(issuerRef)
|
||||
val exitStates = CashSelection.getInstance { serviceHub.jdbcSession().metaData }
|
||||
val exitStates = CashSelection
|
||||
.getInstance { serviceHub.jdbcSession().metaData }
|
||||
.unconsumedCashStatesForSpending(serviceHub, amount, setOf(issuer.party), builder.notary, builder.lockId, setOf(issuer.reference))
|
||||
val signers = try {
|
||||
Cash().generateExit(
|
||||
@ -72,8 +73,8 @@ class CashExitFlow(private val amount: Amount<Currency>,
|
||||
|
||||
// Commit the transaction
|
||||
progressTracker.currentStep = FINALISING_TX
|
||||
finaliseTx(participants, tx, "Unable to notarise exit")
|
||||
return Result(tx, null)
|
||||
val notarised = finaliseTx(tx, participants, "Unable to notarise exit")
|
||||
return Result(notarised, null)
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
|
@ -2,7 +2,6 @@ package net.corda.finance.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.flows.FinalityFlow
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
@ -43,7 +42,8 @@ class CashIssueFlow(private val amount: Amount<Currency>,
|
||||
progressTracker.currentStep = SIGNING_TX
|
||||
val tx = serviceHub.signInitialTransaction(builder, signers)
|
||||
progressTracker.currentStep = FINALISING_TX
|
||||
val notarised = subFlow(FinalityFlow(tx)).single()
|
||||
// There is no one to send the tx to as we're the only participants
|
||||
val notarised = finaliseTx(tx, emptySet(), "Unable to notarise issue")
|
||||
return Result(notarised, ourIdentity)
|
||||
}
|
||||
|
||||
|
@ -45,7 +45,7 @@ open class CashPaymentFlow(
|
||||
}
|
||||
val anonymousRecipient = txIdentities[recipient] ?: recipient
|
||||
progressTracker.currentStep = GENERATING_TX
|
||||
val builder = TransactionBuilder(null as Party?)
|
||||
val builder = TransactionBuilder(notary = null)
|
||||
// TODO: Have some way of restricting this to states the caller controls
|
||||
val (spendTX, keysForSigning) = try {
|
||||
Cash.generateSpend(serviceHub,
|
||||
@ -61,10 +61,13 @@ open class CashPaymentFlow(
|
||||
val tx = serviceHub.signInitialTransaction(spendTX, keysForSigning)
|
||||
|
||||
progressTracker.currentStep = FINALISING_TX
|
||||
finaliseTx(setOf(recipient), tx, "Unable to notarise spend")
|
||||
return Result(tx, anonymousRecipient)
|
||||
val notarised = finaliseTx(tx, setOf(recipient), "Unable to notarise spend")
|
||||
return Result(notarised, anonymousRecipient)
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
class PaymentRequest(amount: Amount<Currency>, val recipient: Party, val anonymous: Boolean, val issuerConstraint: Set<Party> = emptySet()) : AbstractRequest(amount)
|
||||
}
|
||||
class PaymentRequest(amount: Amount<Currency>,
|
||||
val recipient: Party,
|
||||
val anonymous: Boolean,
|
||||
val issuerConstraint: Set<Party> = emptySet()) : AbstractRequest(amount)
|
||||
}
|
||||
|
@ -3,12 +3,8 @@ package net.corda.finance.flows
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.confidential.SwapIdentitiesFlow
|
||||
import net.corda.core.contracts.requireThat
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.flows.CollectSignaturesFlow
|
||||
import net.corda.core.flows.FinalityFlow
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.SignTransactionFlow
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
@ -50,38 +46,38 @@ object TwoPartyDealFlow {
|
||||
|
||||
abstract val payload: Any
|
||||
abstract val notaryParty: Party
|
||||
abstract val otherParty: Party
|
||||
abstract val otherSideSession: FlowSession
|
||||
|
||||
@Suspendable override fun call(): SignedTransaction {
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
progressTracker.currentStep = GENERATING_ID
|
||||
val txIdentities = subFlow(SwapIdentitiesFlow(otherParty))
|
||||
val txIdentities = subFlow(SwapIdentitiesFlow(otherSideSession.counterparty))
|
||||
val anonymousMe = txIdentities[ourIdentity] ?: ourIdentity.anonymise()
|
||||
val anonymousCounterparty = txIdentities[otherParty] ?: otherParty.anonymise()
|
||||
val anonymousCounterparty = txIdentities[otherSideSession.counterparty] ?: otherSideSession.counterparty.anonymise()
|
||||
progressTracker.currentStep = SENDING_PROPOSAL
|
||||
// Make the first message we'll send to kick off the flow.
|
||||
val hello = Handshake(payload, anonymousMe, anonymousCounterparty)
|
||||
// Wait for the FinalityFlow to finish on the other side and return the tx when it's available.
|
||||
send(otherParty, hello)
|
||||
otherSideSession.send(hello)
|
||||
|
||||
val signTransactionFlow = object : SignTransactionFlow(otherParty) {
|
||||
val signTransactionFlow = object : SignTransactionFlow(otherSideSession) {
|
||||
override fun checkTransaction(stx: SignedTransaction) = checkProposal(stx)
|
||||
}
|
||||
|
||||
subFlow(signTransactionFlow)
|
||||
val txId = subFlow(signTransactionFlow).id
|
||||
|
||||
val txHash = receive<SecureHash>(otherParty).unwrap { it }
|
||||
|
||||
return waitForLedgerCommit(txHash)
|
||||
return waitForLedgerCommit(txId)
|
||||
}
|
||||
|
||||
@Suspendable abstract fun checkProposal(stx: SignedTransaction)
|
||||
@Suspendable
|
||||
abstract fun checkProposal(stx: SignedTransaction)
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstracted bilateral deal flow participant that is recipient of initial communication.
|
||||
*/
|
||||
abstract class Secondary<U>(override val progressTracker: ProgressTracker = Secondary.tracker(),
|
||||
val regulators: List<Party> = emptyList()) : FlowLogic<SignedTransaction>() {
|
||||
val regulators: Set<Party> = emptySet()) : FlowLogic<SignedTransaction>() {
|
||||
|
||||
companion object {
|
||||
object RECEIVING : ProgressTracker.Step("Waiting for deal info.")
|
||||
@ -89,13 +85,11 @@ object TwoPartyDealFlow {
|
||||
object SIGNING : ProgressTracker.Step("Generating and signing transaction proposal.")
|
||||
object COLLECTING_SIGNATURES : ProgressTracker.Step("Collecting signatures from other parties.")
|
||||
object RECORDING : ProgressTracker.Step("Recording completed transaction.")
|
||||
object COPYING_TO_REGULATOR : ProgressTracker.Step("Copying regulator.")
|
||||
object COPYING_TO_COUNTERPARTY : ProgressTracker.Step("Copying counterparty.")
|
||||
|
||||
fun tracker() = ProgressTracker(RECEIVING, VERIFYING, SIGNING, COLLECTING_SIGNATURES, RECORDING, COPYING_TO_REGULATOR, COPYING_TO_COUNTERPARTY)
|
||||
fun tracker() = ProgressTracker(RECEIVING, VERIFYING, SIGNING, COLLECTING_SIGNATURES, RECORDING)
|
||||
}
|
||||
|
||||
abstract val otherParty: Party
|
||||
abstract val otherSideSession: FlowSession
|
||||
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
@ -109,31 +103,24 @@ object TwoPartyDealFlow {
|
||||
serviceHub.signInitialTransaction(utx, additionalSigningPubKeys)
|
||||
}
|
||||
|
||||
logger.trace { "Signed proposed transaction." }
|
||||
logger.trace("Signed proposed transaction.")
|
||||
|
||||
progressTracker.currentStep = COLLECTING_SIGNATURES
|
||||
|
||||
// Get signature of initiating side
|
||||
val ptxSignedByOtherSide = ptx + subFlow(CollectSignatureFlow(ptx, otherSideSession, otherSideSession.counterparty.owningKey))
|
||||
|
||||
// DOCSTART 1
|
||||
val stx = subFlow(CollectSignaturesFlow(ptx, additionalSigningPubKeys))
|
||||
// Get signatures of other signers
|
||||
val sessionsForOtherSigners = serviceHub.excludeNotary(serviceHub.groupPublicKeysByWellKnownParty(ptxSignedByOtherSide.getMissingSigners()), ptxSignedByOtherSide).map { initiateFlow(it.key) }
|
||||
val stx = subFlow(CollectSignaturesFlow(ptxSignedByOtherSide, sessionsForOtherSigners, additionalSigningPubKeys))
|
||||
// DOCEND 1
|
||||
|
||||
logger.trace { "Got signatures from other party, verifying ... " }
|
||||
logger.trace("Got signatures from other party, verifying ... ")
|
||||
|
||||
progressTracker.currentStep = RECORDING
|
||||
val ftx = subFlow(FinalityFlow(stx, setOf(otherParty, ourIdentity))).single()
|
||||
|
||||
logger.trace { "Recorded transaction." }
|
||||
|
||||
progressTracker.currentStep = COPYING_TO_REGULATOR
|
||||
|
||||
// Copy the transaction to every regulator in the network. This is obviously completely bogus, it's
|
||||
// just for demo purposes.
|
||||
regulators.forEach { send(it, ftx) }
|
||||
|
||||
progressTracker.currentStep = COPYING_TO_COUNTERPARTY
|
||||
// Send the final transaction hash back to the other party.
|
||||
// We need this so we don't break the IRS demo and the SIMM Demo.
|
||||
send(otherParty, ftx.id)
|
||||
val ftx = subFlow(FinalityFlow(stx, regulators + otherSideSession.counterparty))
|
||||
logger.trace("Recorded transaction.")
|
||||
|
||||
return ftx
|
||||
}
|
||||
@ -142,14 +129,14 @@ object TwoPartyDealFlow {
|
||||
private fun receiveAndValidateHandshake(): Handshake<U> {
|
||||
progressTracker.currentStep = RECEIVING
|
||||
// Wait for a trade request to come in on our pre-provided session ID.
|
||||
val handshake = receive<Handshake<U>>(otherParty)
|
||||
val handshake = otherSideSession.receive<Handshake<U>>()
|
||||
|
||||
progressTracker.currentStep = VERIFYING
|
||||
return handshake.unwrap {
|
||||
// Verify the transaction identities represent the correct parties
|
||||
val wellKnownOtherParty = serviceHub.identityService.partyFromAnonymous(it.primaryIdentity)
|
||||
val wellKnownMe = serviceHub.identityService.partyFromAnonymous(it.secondaryIdentity)
|
||||
require(wellKnownOtherParty == otherParty)
|
||||
require(wellKnownOtherParty == otherSideSession.counterparty)
|
||||
require(wellKnownMe == ourIdentity)
|
||||
validateHandshake(it)
|
||||
}
|
||||
@ -167,7 +154,7 @@ object TwoPartyDealFlow {
|
||||
/**
|
||||
* One side of the flow for inserting a pre-agreed deal.
|
||||
*/
|
||||
open class Instigator(override val otherParty: Party,
|
||||
open class Instigator(override val otherSideSession: FlowSession,
|
||||
override val payload: AutoOffer,
|
||||
override val progressTracker: ProgressTracker = Primary.tracker()) : Primary() {
|
||||
override val notaryParty: Party get() = payload.notary
|
||||
@ -180,7 +167,7 @@ object TwoPartyDealFlow {
|
||||
/**
|
||||
* One side of the flow for inserting a pre-agreed deal.
|
||||
*/
|
||||
open class Acceptor(override val otherParty: Party,
|
||||
open class Acceptor(override val otherSideSession: FlowSession,
|
||||
override val progressTracker: ProgressTracker = Secondary.tracker()) : Secondary<AutoOffer>() {
|
||||
|
||||
override fun validateHandshake(handshake: Handshake<AutoOffer>): Handshake<AutoOffer> {
|
||||
|
@ -55,10 +55,10 @@ object TwoPartyTradeFlow {
|
||||
val payToIdentity: PartyAndCertificate
|
||||
)
|
||||
|
||||
open class Seller(val otherParty: Party,
|
||||
val assetToSell: StateAndRef<OwnableState>,
|
||||
val price: Amount<Currency>,
|
||||
val myParty: PartyAndCertificate, // TODO Left because in tests it's used to pass anonymous party.
|
||||
open class Seller(private val otherSideSession: FlowSession,
|
||||
private val assetToSell: StateAndRef<OwnableState>,
|
||||
private val price: Amount<Currency>,
|
||||
private val myParty: PartyAndCertificate, // TODO Left because in tests it's used to pass anonymous party.
|
||||
override val progressTracker: ProgressTracker = Seller.tracker()) : FlowLogic<SignedTransaction>() {
|
||||
|
||||
companion object {
|
||||
@ -80,22 +80,22 @@ object TwoPartyTradeFlow {
|
||||
val hello = SellerTradeInfo(price, myParty)
|
||||
// What we get back from the other side is a transaction that *might* be valid and acceptable to us,
|
||||
// but we must check it out thoroughly before we sign!
|
||||
// SendTransactionFlow allows otherParty to access our data to resolve the transaction.
|
||||
subFlow(SendStateAndRefFlow(otherParty, listOf(assetToSell)))
|
||||
send(otherParty, hello)
|
||||
// SendTransactionFlow allows seller to access our data to resolve the transaction.
|
||||
subFlow(SendStateAndRefFlow(otherSideSession, listOf(assetToSell)))
|
||||
otherSideSession.send(hello)
|
||||
|
||||
// Verify and sign the transaction.
|
||||
progressTracker.currentStep = VERIFYING_AND_SIGNING
|
||||
|
||||
// Sync identities to ensure we know all of the identities involved in the transaction we're about to
|
||||
// be asked to sign
|
||||
subFlow(IdentitySyncFlow.Receive(otherParty))
|
||||
subFlow(IdentitySyncFlow.Receive(otherSideSession))
|
||||
|
||||
// DOCSTART 5
|
||||
val signTransactionFlow = object : SignTransactionFlow(otherParty, VERIFYING_AND_SIGNING.childProgressTracker()) {
|
||||
val signTransactionFlow = object : SignTransactionFlow(otherSideSession, VERIFYING_AND_SIGNING.childProgressTracker()) {
|
||||
override fun checkTransaction(stx: SignedTransaction) {
|
||||
// Verify that we know who all the participants in the transaction are
|
||||
val states: Iterable<ContractState> = (stx.tx.inputs.map { serviceHub.loadState(it).data } + stx.tx.outputs.map { it.data })
|
||||
val states: Iterable<ContractState> = stx.tx.inputs.map { serviceHub.loadState(it).data } + stx.tx.outputs.map { it.data }
|
||||
states.forEach { state ->
|
||||
state.participants.forEach { anon ->
|
||||
require(serviceHub.identityService.partyFromAnonymous(anon) != null) {
|
||||
@ -108,8 +108,11 @@ object TwoPartyTradeFlow {
|
||||
throw FlowException("Transaction is not sending us the right amount of cash")
|
||||
}
|
||||
}
|
||||
return subFlow(signTransactionFlow)
|
||||
|
||||
val txId = subFlow(signTransactionFlow).id
|
||||
// DOCEND 5
|
||||
|
||||
return waitForLedgerCommit(txId)
|
||||
}
|
||||
// DOCEND 4
|
||||
|
||||
@ -126,13 +129,13 @@ object TwoPartyTradeFlow {
|
||||
// express flow state machines on top of the messaging layer.
|
||||
}
|
||||
|
||||
open class Buyer(private val otherParty: Party,
|
||||
open class Buyer(private val sellerSession: FlowSession,
|
||||
private val notary: Party,
|
||||
private val acceptablePrice: Amount<Currency>,
|
||||
private val typeToBuy: Class<out OwnableState>,
|
||||
private val anonymous: Boolean) : FlowLogic<SignedTransaction>() {
|
||||
constructor(otherParty: Party, notary: Party, acceptablePrice: Amount<Currency>, typeToBuy: Class<out OwnableState>) :
|
||||
this(otherParty, notary, acceptablePrice, typeToBuy, true)
|
||||
constructor(otherSideSession: FlowSession, notary: Party, acceptablePrice: Amount<Currency>, typeToBuy: Class<out OwnableState>) :
|
||||
this(otherSideSession, notary, acceptablePrice, typeToBuy, true)
|
||||
// DOCSTART 2
|
||||
object RECEIVING : ProgressTracker.Step("Waiting for seller trading info")
|
||||
|
||||
@ -170,22 +173,23 @@ object TwoPartyTradeFlow {
|
||||
val partSignedTx = serviceHub.signInitialTransaction(ptx, cashSigningPubKeys)
|
||||
|
||||
// Sync up confidential identities in the transaction with our counterparty
|
||||
subFlow(IdentitySyncFlow.Send(otherParty, ptx.toWireTransaction()))
|
||||
subFlow(IdentitySyncFlow.Send(sellerSession, ptx.toWireTransaction()))
|
||||
|
||||
// Send the signed transaction to the seller, who must then sign it themselves and commit
|
||||
// it to the ledger by sending it to the notary.
|
||||
progressTracker.currentStep = COLLECTING_SIGNATURES
|
||||
val twiceSignedTx = subFlow(CollectSignaturesFlow(partSignedTx, cashSigningPubKeys, COLLECTING_SIGNATURES.childProgressTracker()))
|
||||
val sellerSignature = subFlow(CollectSignatureFlow(partSignedTx, sellerSession, sellerSession.counterparty.owningKey))
|
||||
val twiceSignedTx = partSignedTx + sellerSignature
|
||||
|
||||
// Notarise and record the transaction.
|
||||
progressTracker.currentStep = RECORDING
|
||||
return subFlow(FinalityFlow(twiceSignedTx)).single()
|
||||
return subFlow(FinalityFlow(twiceSignedTx))
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun receiveAndValidateTradeRequest(): Pair<StateAndRef<OwnableState>, SellerTradeInfo> {
|
||||
val assetForSale = subFlow(ReceiveStateAndRefFlow<OwnableState>(otherParty)).single()
|
||||
return assetForSale to receive<SellerTradeInfo>(otherParty).unwrap {
|
||||
val assetForSale = subFlow(ReceiveStateAndRefFlow<OwnableState>(sellerSession)).single()
|
||||
return assetForSale to sellerSession.receive<SellerTradeInfo>().unwrap {
|
||||
progressTracker.currentStep = VERIFYING
|
||||
// What is the seller trying to sell us?
|
||||
val asset = assetForSale.state.data
|
||||
@ -194,12 +198,12 @@ object TwoPartyTradeFlow {
|
||||
// The asset must either be owned by the well known identity of the counterparty, or we must be able to
|
||||
// prove the owner is a confidential identity of the counterparty.
|
||||
val assetForSaleIdentity = serviceHub.identityService.partyFromAnonymous(asset.owner)
|
||||
require(assetForSaleIdentity == otherParty)
|
||||
require(assetForSaleIdentity == sellerSession.counterparty)
|
||||
|
||||
// Register the identity we're about to send payment to. This shouldn't be the same as the asset owner
|
||||
// identity, so that anonymity is enforced.
|
||||
val wellKnownPayToIdentity = serviceHub.identityService.verifyAndRegisterIdentity(it.payToIdentity)
|
||||
require(wellKnownPayToIdentity?.party == otherParty) { "Well known identity to pay to must match counterparty identity" }
|
||||
require(wellKnownPayToIdentity?.party == sellerSession.counterparty) { "Well known identity to pay to must match counterparty identity" }
|
||||
|
||||
if (it.price > acceptablePrice)
|
||||
throw UnacceptablePriceException(it.price)
|
||||
|
@ -1,7 +1,7 @@
|
||||
package net.corda.finance.flows;
|
||||
|
||||
import net.corda.core.flows.AbstractStateReplacementFlow;
|
||||
import net.corda.core.identity.Party;
|
||||
import net.corda.core.flows.FlowSession;
|
||||
import net.corda.core.transactions.SignedTransaction;
|
||||
import net.corda.core.utilities.ProgressTracker;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@ -11,8 +11,8 @@ public class AbstractStateReplacementFlowTest {
|
||||
|
||||
// Acceptor used to have a type parameter of Unit which prevented Java code from subclassing it (https://youtrack.jetbrains.com/issue/KT-15964).
|
||||
private static class TestAcceptorCanBeInheritedInJava extends AbstractStateReplacementFlow.Acceptor {
|
||||
public TestAcceptorCanBeInheritedInJava(@NotNull Party otherSide, @NotNull ProgressTracker progressTracker) {
|
||||
super(otherSide, progressTracker);
|
||||
public TestAcceptorCanBeInheritedInJava(@NotNull FlowSession otherSideSession, @NotNull ProgressTracker progressTracker) {
|
||||
super(otherSideSession, progressTracker);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -38,10 +38,9 @@ class CashPaymentFlowTests {
|
||||
notaryNode = nodes.notaryNode
|
||||
bankOfCordaNode = nodes.partyNodes[0]
|
||||
bankOfCorda = bankOfCordaNode.info.chooseIdentity()
|
||||
|
||||
mockNet.runNetwork()
|
||||
notary = bankOfCordaNode.services.getDefaultNotary()
|
||||
notary = notaryNode.services.getDefaultNotary()
|
||||
val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance, ref, notary)).resultFuture
|
||||
mockNet.runNetwork()
|
||||
future.getOrThrow()
|
||||
}
|
||||
|
||||
|
@ -1,19 +1,16 @@
|
||||
package net.corda.node
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.InitiatedBy
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.concurrent.transpose
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.testing.ALICE
|
||||
import net.corda.testing.BOB
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.node.services.FlowPermissions.Companion.startFlowPermission
|
||||
import net.corda.nodeapi.User
|
||||
import net.corda.testing.ALICE
|
||||
import net.corda.testing.BOB
|
||||
import net.corda.testing.chooseIdentity
|
||||
import net.corda.testing.driver.driver
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
@ -41,15 +38,15 @@ class CordappScanningDriverTest {
|
||||
@InitiatingFlow
|
||||
class ReceiveFlow(val otherParty: Party) :FlowLogic<String>() {
|
||||
@Suspendable
|
||||
override fun call(): String = receive<String>(otherParty).unwrap { it }
|
||||
override fun call(): String = initiateFlow(otherParty).receive<String>().unwrap { it }
|
||||
}
|
||||
|
||||
@InitiatedBy(ReceiveFlow::class)
|
||||
open class SendClassFlow(val otherParty: Party) : FlowLogic<Unit>() {
|
||||
open class SendClassFlow(val otherPartySession: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() = send(otherParty, javaClass.name)
|
||||
override fun call() = otherPartySession.send(javaClass.name)
|
||||
}
|
||||
|
||||
@InitiatedBy(ReceiveFlow::class)
|
||||
class SendSubClassFlow(otherParty: Party) : SendClassFlow(otherParty)
|
||||
class SendSubClassFlow(otherPartySession: FlowSession) : SendClassFlow(otherPartySession)
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package net.corda.node.services.statemachine
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.concurrent.transpose
|
||||
@ -32,17 +33,18 @@ class FlowVersioningTest : NodeBasedTest() {
|
||||
@Suspendable
|
||||
override fun call(): Pair<Int, Int> {
|
||||
// Execute receive() outside of the Pair constructor to avoid Kotlin/Quasar instrumentation bug.
|
||||
val alicePlatformVersionAccordingToBob = receive<Int>(initiatedParty).unwrap { it }
|
||||
val session = initiateFlow(initiatedParty)
|
||||
val alicePlatformVersionAccordingToBob = session.receive<Int>().unwrap { it }
|
||||
return Pair(
|
||||
alicePlatformVersionAccordingToBob,
|
||||
getFlowInfo(initiatedParty).flowVersion
|
||||
session.getCounterpartyFlowInfo().flowVersion
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class PretendInitiatedCoreFlow(val initiatingParty: Party) : FlowLogic<Unit>() {
|
||||
private class PretendInitiatedCoreFlow(val otherSideSession: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() = send(initiatingParty, getFlowInfo(initiatingParty).flowVersion)
|
||||
override fun call() = otherSideSession.send(otherSideSession.getCounterpartyFlowInfo().flowVersion)
|
||||
}
|
||||
|
||||
}
|
@ -3,7 +3,6 @@ package net.corda.node.services.statemachine
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.InputStreamAndHash
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
@ -39,18 +38,19 @@ class LargeTransactionsTest {
|
||||
val stx = serviceHub.signInitialTransaction(tx, ourIdentity.owningKey)
|
||||
// Send to the other side and wait for it to trigger resolution from us.
|
||||
val bob = serviceHub.identityService.partyFromX500Name(BOB.name)!!
|
||||
subFlow(SendTransactionFlow(bob, stx))
|
||||
receive<Unit>(bob)
|
||||
val bobSession = initiateFlow(bob)
|
||||
subFlow(SendTransactionFlow(bobSession, stx))
|
||||
bobSession.receive<Unit>()
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(SendLargeTransactionFlow::class) @Suppress("UNUSED")
|
||||
class ReceiveLargeTransactionFlow(private val counterParty: Party) : FlowLogic<Unit>() {
|
||||
class ReceiveLargeTransactionFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
subFlow(ReceiveTransactionFlow(counterParty))
|
||||
subFlow(ReceiveTransactionFlow(otherSide))
|
||||
// Unblock the other side by sending some dummy object (Unit is fine here as it's a singleton).
|
||||
send(counterParty, Unit)
|
||||
otherSide.send(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,17 +3,16 @@ package net.corda.services.messaging
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.client.rpc.CordaRPCClient
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.utilities.toBase58String
|
||||
import net.corda.core.crypto.random63BitValue
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.flows.InitiatedBy
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.crypto.random63BitValue
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.testing.ALICE
|
||||
import net.corda.testing.BOB
|
||||
import net.corda.core.utilities.toBase58String
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.node.internal.Node
|
||||
import net.corda.node.internal.StartedNode
|
||||
@ -25,6 +24,8 @@ import net.corda.nodeapi.ArtemisMessagingComponent.Companion.PEERS_PREFIX
|
||||
import net.corda.nodeapi.RPCApi
|
||||
import net.corda.nodeapi.User
|
||||
import net.corda.nodeapi.config.SSLConfiguration
|
||||
import net.corda.testing.ALICE
|
||||
import net.corda.testing.BOB
|
||||
import net.corda.testing.chooseIdentity
|
||||
import net.corda.testing.configureTestSSL
|
||||
import net.corda.testing.messaging.SimpleMQClient
|
||||
@ -229,12 +230,12 @@ abstract class MQSecurityTest : NodeBasedTest() {
|
||||
@InitiatingFlow
|
||||
private class SendFlow(val otherParty: Party, val payload: Any) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() = send(otherParty, payload)
|
||||
override fun call() = initiateFlow(otherParty).send(payload)
|
||||
}
|
||||
|
||||
@InitiatedBy(SendFlow::class)
|
||||
private class ReceiveFlow(val otherParty: Party) : FlowLogic<Any>() {
|
||||
private class ReceiveFlow(val otherPartySession: FlowSession) : FlowLogic<Any>() {
|
||||
@Suspendable
|
||||
override fun call() = receive<Any>(otherParty).unwrap { it }
|
||||
override fun call() = otherPartySession.receive<Any>().unwrap { it }
|
||||
}
|
||||
}
|
@ -18,8 +18,8 @@ import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.node.services.FlowPermissions
|
||||
import net.corda.nodeapi.ServiceInfo
|
||||
import net.corda.node.services.transactions.SimpleNotaryService
|
||||
import net.corda.nodeapi.ServiceInfo
|
||||
import net.corda.nodeapi.User
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
import net.corda.testing.chooseIdentity
|
||||
@ -151,6 +151,6 @@ class SendMessageFlow(private val message: Message) : FlowLogic<SignedTransactio
|
||||
val signedTx = serviceHub.signInitialTransaction(txBuilder)
|
||||
|
||||
progressTracker.currentStep = FINALISING_TRANSACTION
|
||||
return subFlow(FinalityFlow(signedTx, FINALISING_TRANSACTION.childProgressTracker())).single()
|
||||
return subFlow(FinalityFlow(signedTx, FINALISING_TRANSACTION.childProgressTracker()))
|
||||
}
|
||||
}
|
@ -36,8 +36,8 @@ import net.corda.core.utilities.debug
|
||||
import net.corda.node.internal.classloading.requireAnnotation
|
||||
import net.corda.node.internal.cordapp.CordappLoader
|
||||
import net.corda.node.internal.cordapp.CordappProvider
|
||||
import net.corda.node.services.FinalityHandler
|
||||
import net.corda.node.services.NotaryChangeHandler
|
||||
import net.corda.node.services.NotifyTransactionHandler
|
||||
import net.corda.node.services.api.*
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.config.configureWithDevSSLCertificate
|
||||
@ -360,15 +360,8 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
||||
* compatibility [flowFactory] provides a second parameter which is the platform version of the initiating party.
|
||||
* @suppress
|
||||
*/
|
||||
@Deprecated("Use installCoreFlowExpectingFlowSession() instead")
|
||||
@VisibleForTesting
|
||||
fun installCoreFlow(clientFlowClass: KClass<out FlowLogic<*>>, flowFactory: (Party) -> FlowLogic<*>) {
|
||||
log.warn(deprecatedFlowConstructorMessage(clientFlowClass.java))
|
||||
installCoreFlowExpectingFlowSession(clientFlowClass, { flowSession -> flowFactory(flowSession.counterparty) })
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun installCoreFlowExpectingFlowSession(clientFlowClass: KClass<out FlowLogic<*>>, flowFactory: (FlowSession) -> FlowLogic<*>) {
|
||||
fun installCoreFlow(clientFlowClass: KClass<out FlowLogic<*>>, flowFactory: (FlowSession) -> FlowLogic<*>) {
|
||||
require(clientFlowClass.java.flowVersionAndInitiatingClass.first == 1) {
|
||||
"${InitiatingFlow::class.java.name}.version not applicable for core flows; their version is the node's platform version"
|
||||
}
|
||||
@ -378,7 +371,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
||||
|
||||
|
||||
private fun installCoreFlows() {
|
||||
installCoreFlow(BroadcastTransactionFlow::class, ::NotifyTransactionHandler)
|
||||
installCoreFlow(FinalityFlow::class, ::FinalityHandler)
|
||||
installCoreFlow(NotaryChangeFlow::class, ::NotaryChangeHandler)
|
||||
installCoreFlow(ContractUpgradeFlow.Initiator::class, ::Acceptor)
|
||||
installCoreFlow(SwapIdentitiesFlow::class, ::SwapIdentitiesHandler)
|
||||
@ -407,10 +400,10 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
||||
}
|
||||
|
||||
private fun makeCordappLoader(): CordappLoader {
|
||||
val scanPackage = System.getProperty("net.corda.node.cordapp.scan.package")
|
||||
return if (scanPackage != null) {
|
||||
val scanPackages = System.getProperty("net.corda.node.cordapp.scan.packages")
|
||||
return if (scanPackages != null) {
|
||||
check(configuration.devMode) { "Package scanning can only occur in dev mode" }
|
||||
CordappLoader.createDevMode(scanPackage)
|
||||
CordappLoader.createDevMode(scanPackages)
|
||||
} else {
|
||||
CordappLoader.createDefault(configuration.baseDirectory)
|
||||
}
|
||||
|
@ -57,19 +57,22 @@ class CordappLoader private constructor(private val cordappJarPaths: List<URL>)
|
||||
* @param scanPackage Resolves the JARs that contain scanPackage and use them as the source for
|
||||
* the classpath scanning.
|
||||
*/
|
||||
fun createDevMode(scanPackage: String): CordappLoader {
|
||||
val resource = scanPackage.replace('.', '/')
|
||||
val paths = this::class.java.classLoader.getResources(resource)
|
||||
.asSequence()
|
||||
.map {
|
||||
val uri = if (it.protocol == "jar") {
|
||||
(it.openConnection() as JarURLConnection).jarFileURL.toURI()
|
||||
} else {
|
||||
URI(it.toExternalForm().removeSuffix(resource))
|
||||
fun createDevMode(scanPackages: String): CordappLoader {
|
||||
val paths = scanPackages.split(",").flatMap { scanPackage ->
|
||||
val resource = scanPackage.replace('.', '/')
|
||||
this::class.java.classLoader.getResources(resource)
|
||||
.asSequence()
|
||||
.map {
|
||||
val uri = if (it.protocol == "jar") {
|
||||
(it.openConnection() as JarURLConnection).jarFileURL.toURI()
|
||||
} else {
|
||||
URI(it.toExternalForm().removeSuffix(resource))
|
||||
}
|
||||
uri.toURL()
|
||||
}
|
||||
uri.toURL()
|
||||
}
|
||||
.toList()
|
||||
.toList()
|
||||
}
|
||||
|
||||
return CordappLoader(paths)
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,7 @@
|
||||
package net.corda.node.services
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.flows.AbstractStateReplacementFlow
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.ReceiveTransactionFlow
|
||||
import net.corda.core.flows.StateReplacementException
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
|
||||
@ -12,15 +9,15 @@ import net.corda.core.transactions.SignedTransaction
|
||||
// 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 NotifyTransactionHandler(val otherParty: Party) : FlowLogic<Unit>() {
|
||||
class FinalityHandler(private val sender: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val stx = subFlow(ReceiveTransactionFlow(otherParty))
|
||||
val stx = subFlow(ReceiveTransactionFlow(sender))
|
||||
serviceHub.recordTransactions(stx)
|
||||
}
|
||||
}
|
||||
|
||||
class NotaryChangeHandler(otherSide: Party) : AbstractStateReplacementFlow.Acceptor<Party>(otherSide) {
|
||||
class NotaryChangeHandler(otherSideSession: FlowSession) : AbstractStateReplacementFlow.Acceptor<Party>(otherSideSession) {
|
||||
/**
|
||||
* Check the notary change proposal.
|
||||
*
|
||||
|
@ -18,7 +18,7 @@ class FlowSessionInternal(
|
||||
val ourSessionId: Long,
|
||||
val initiatingParty: Party?,
|
||||
var state: FlowSessionState,
|
||||
val retryable: Boolean = false) {
|
||||
var retryable: Boolean = false) {
|
||||
val receivedMessages = ConcurrentLinkedQueue<ReceivedSessionMessage<*>>()
|
||||
val fiber: FlowStateMachineImpl<*> get() = flow.stateMachine as FlowStateMachineImpl<*>
|
||||
|
||||
@ -30,14 +30,19 @@ class FlowSessionInternal(
|
||||
/**
|
||||
* [FlowSessionState] describes the session's state.
|
||||
*
|
||||
* [Initiating] is pre-handshake. [Initiating.otherParty] at this point holds a [Party] corresponding to either a
|
||||
* specific peer or a service.
|
||||
* [Uninitiated] is pre-handshake, where no communication has happened. [Initiating.otherParty] at this point holds a
|
||||
* [Party] corresponding to either a specific peer or a service.
|
||||
* [Initiating] is pre-handshake, where the initiating message has been sent.
|
||||
* [Initiated] is post-handshake. At this point [Initiating.otherParty] will have been resolved to a specific peer
|
||||
* [Initiated.peerParty], and the peer's sessionId has been initialised.
|
||||
*/
|
||||
sealed class FlowSessionState {
|
||||
abstract val sendToParty: Party
|
||||
|
||||
data class Uninitiated(val otherParty: Party) : FlowSessionState() {
|
||||
override val sendToParty: Party get() = otherParty
|
||||
}
|
||||
|
||||
/** [otherParty] may be a specific peer or a service party */
|
||||
data class Initiating(val otherParty: Party) : FlowSessionState() {
|
||||
override val sendToParty: Party get() = otherParty
|
||||
|
@ -164,6 +164,16 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
||||
|
||||
@Suspendable
|
||||
override fun initiateFlow(otherParty: Party, sessionFlow: FlowLogic<*>): FlowSession {
|
||||
val sessionKey = Pair(sessionFlow, otherParty)
|
||||
if (openSessions.containsKey(sessionKey)) {
|
||||
throw IllegalStateException(
|
||||
"Attempted to initiateFlow() twice in the same InitiatingFlow $sessionFlow for the same party " +
|
||||
"$otherParty. This isn't supported in this version of Corda. Alternatively you may " +
|
||||
"initiate a new flow by calling initiateFlow() in an " +
|
||||
"@${InitiatingFlow::class.java.simpleName} sub-flow."
|
||||
)
|
||||
}
|
||||
createNewSession(otherParty, sessionFlow)
|
||||
val flowSession = FlowSessionImpl(otherParty)
|
||||
flowSession.stateMachine = this
|
||||
flowSession.sessionFlow = sessionFlow
|
||||
@ -186,7 +196,7 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
||||
logger.debug { "sendAndReceive(${receiveType.name}, $otherParty, ${payload.toString().abbreviate(300)}) ..." }
|
||||
val session = getConfirmedSessionIfPresent(otherParty, sessionFlow)
|
||||
val receivedSessionData: ReceivedSessionMessage<SessionData> = if (session == null) {
|
||||
val newSession = startNewSession(otherParty, sessionFlow, payload, waitForConfirmation = true, retryable = retrySend)
|
||||
val newSession = initiateSession(otherParty, sessionFlow, payload, waitForConfirmation = true, retryable = retrySend)
|
||||
// Only do a receive here as the session init has carried the payload
|
||||
receiveInternal(newSession, receiveType)
|
||||
} else {
|
||||
@ -221,7 +231,7 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
||||
val session = getConfirmedSessionIfPresent(otherParty, sessionFlow)
|
||||
if (session == null) {
|
||||
// Don't send the payload again if it was already piggy-backed on a session init
|
||||
startNewSession(otherParty, sessionFlow, payload, waitForConfirmation = false)
|
||||
initiateSession(otherParty, sessionFlow, payload, waitForConfirmation = false)
|
||||
} else {
|
||||
sendInternal(session, createSessionData(session, payload))
|
||||
}
|
||||
@ -308,8 +318,8 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
||||
private fun createSessionData(session: FlowSessionInternal, payload: Any): SessionData {
|
||||
val sessionState = session.state
|
||||
val peerSessionId = when (sessionState) {
|
||||
is FlowSessionState.Initiating -> throw IllegalStateException("We've somehow held onto an unconfirmed session: $session")
|
||||
is FlowSessionState.Initiated -> sessionState.peerSessionId
|
||||
else -> throw IllegalStateException("We've somehow held onto a non-initiated session: $session")
|
||||
}
|
||||
return SessionData(peerSessionId, payload)
|
||||
}
|
||||
@ -332,37 +342,53 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
||||
|
||||
@Suspendable
|
||||
private fun getConfirmedSessionIfPresent(otherParty: Party, sessionFlow: FlowLogic<*>): FlowSessionInternal? {
|
||||
return openSessions[Pair(sessionFlow, otherParty)]?.apply {
|
||||
if (state is FlowSessionState.Initiating) {
|
||||
// Session still initiating, wait for the confirmation
|
||||
waitForConfirmation()
|
||||
val session = openSessions[Pair(sessionFlow, otherParty)] ?: return null
|
||||
return when (session.state) {
|
||||
is FlowSessionState.Uninitiated -> null
|
||||
is FlowSessionState.Initiating -> {
|
||||
session.waitForConfirmation()
|
||||
session
|
||||
}
|
||||
is FlowSessionState.Initiated -> session
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun getConfirmedSession(otherParty: Party, sessionFlow: FlowLogic<*>): FlowSessionInternal {
|
||||
return getConfirmedSessionIfPresent(otherParty, sessionFlow) ?:
|
||||
startNewSession(otherParty, sessionFlow, null, waitForConfirmation = true)
|
||||
initiateSession(otherParty, sessionFlow, null, waitForConfirmation = true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new session. The provided [otherParty] can be an identity of any advertised service on the network,
|
||||
* and might be advertised by more than one node. Therefore we first choose a single node that advertises it
|
||||
* and use its *legal identity* for communication. At the moment a single node can compose its legal identity out of
|
||||
* multiple public keys, but we **don't support multiple nodes advertising the same legal identity**.
|
||||
*/
|
||||
@Suspendable
|
||||
private fun startNewSession(otherParty: Party,
|
||||
sessionFlow: FlowLogic<*>,
|
||||
firstPayload: Any?,
|
||||
waitForConfirmation: Boolean,
|
||||
retryable: Boolean = false): FlowSessionInternal {
|
||||
logger.trace { "Initiating a new session with $otherParty" }
|
||||
val session = FlowSessionInternal(sessionFlow, random63BitValue(), null, FlowSessionState.Initiating(otherParty), retryable)
|
||||
private fun createNewSession(
|
||||
otherParty: Party,
|
||||
sessionFlow: FlowLogic<*>
|
||||
) {
|
||||
logger.trace { "Creating a new session with $otherParty" }
|
||||
val session = FlowSessionInternal(sessionFlow, random63BitValue(), null, FlowSessionState.Uninitiated(otherParty))
|
||||
openSessions[Pair(sessionFlow, otherParty)] = session
|
||||
val (version, initiatingFlowClass) = sessionFlow.javaClass.flowVersionAndInitiatingClass
|
||||
val sessionInit = SessionInit(session.ourSessionId, initiatingFlowClass.name, version, sessionFlow.javaClass.appName, firstPayload)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun initiateSession(
|
||||
otherParty: Party,
|
||||
sessionFlow: FlowLogic<*>,
|
||||
firstPayload: Any?,
|
||||
waitForConfirmation: Boolean,
|
||||
retryable: Boolean = false
|
||||
): FlowSessionInternal {
|
||||
val session = openSessions[Pair(sessionFlow, otherParty)]
|
||||
if (session == null) {
|
||||
throw IllegalStateException("Expected an Uninitiated session for $otherParty")
|
||||
}
|
||||
val state = session.state
|
||||
if (state !is FlowSessionState.Uninitiated) {
|
||||
throw IllegalStateException("Tried to initiate a session $session, but it's already initiating/initiated")
|
||||
}
|
||||
logger.trace { "Initiating a new session with ${state.otherParty}" }
|
||||
session.state = FlowSessionState.Initiating(state.otherParty)
|
||||
session.retryable = retryable
|
||||
val (version, initiatingFlowClass) = session.flow.javaClass.flowVersionAndInitiatingClass
|
||||
val sessionInit = SessionInit(session.ourSessionId, initiatingFlowClass.name, version, session.flow.javaClass.appName, firstPayload)
|
||||
sendInternal(session, sessionInit)
|
||||
if (waitForConfirmation) {
|
||||
session.waitForConfirmation()
|
||||
|
@ -6,6 +6,7 @@ import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.DigitalSignature
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.flows.NotaryError
|
||||
import net.corda.core.flows.NotaryException
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
@ -71,19 +72,19 @@ class BFTNonValidatingNotaryService(override val services: ServiceHubInternal,
|
||||
|
||||
fun commitTransaction(tx: Any, otherSide: Party) = client.commitTransaction(tx, otherSide)
|
||||
|
||||
override fun createServiceFlow(otherParty: Party): FlowLogic<Void?> = ServiceFlow(otherParty, this)
|
||||
override fun createServiceFlow(otherPartySession: FlowSession): FlowLogic<Void?> = ServiceFlow(otherPartySession, this)
|
||||
|
||||
private class ServiceFlow(val otherSide: Party, val service: BFTNonValidatingNotaryService) : FlowLogic<Void?>() {
|
||||
private class ServiceFlow(val otherSideSession: FlowSession, val service: BFTNonValidatingNotaryService) : FlowLogic<Void?>() {
|
||||
@Suspendable
|
||||
override fun call(): Void? {
|
||||
val stx = receive<FilteredTransaction>(otherSide).unwrap { it }
|
||||
val stx = otherSideSession.receive<FilteredTransaction>().unwrap { it }
|
||||
val signatures = commit(stx)
|
||||
send(otherSide, signatures)
|
||||
otherSideSession.send(signatures)
|
||||
return null
|
||||
}
|
||||
|
||||
private fun commit(stx: FilteredTransaction): List<DigitalSignature> {
|
||||
val response = service.commitTransaction(stx, otherSide)
|
||||
val response = service.commitTransaction(stx, otherSideSession.counterparty)
|
||||
when (response) {
|
||||
is BFTSMaRt.ClusterResponse.Error -> throw NotaryException(response.error)
|
||||
is BFTSMaRt.ClusterResponse.Signatures -> {
|
||||
|
@ -1,16 +1,16 @@
|
||||
package net.corda.node.services.transactions
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.contracts.ComponentGroupEnum
|
||||
import net.corda.core.flows.NotaryFlow
|
||||
import net.corda.core.flows.TransactionParts
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.services.TrustedAuthorityNotaryService
|
||||
import net.corda.core.transactions.FilteredTransaction
|
||||
import net.corda.core.transactions.NotaryChangeWireTransaction
|
||||
import net.corda.core.utilities.unwrap
|
||||
|
||||
class NonValidatingNotaryFlow(otherSide: Party, service: TrustedAuthorityNotaryService) : NotaryFlow.Service(otherSide, service) {
|
||||
class NonValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAuthorityNotaryService) : NotaryFlow.Service(otherSideSession, service) {
|
||||
/**
|
||||
* The received transaction is not checked for contract-validity, as that would require fully
|
||||
* resolving it into a [TransactionForVerification], for which the caller would have to reveal the whole transaction
|
||||
@ -21,7 +21,7 @@ class NonValidatingNotaryFlow(otherSide: Party, service: TrustedAuthorityNotaryS
|
||||
*/
|
||||
@Suspendable
|
||||
override fun receiveAndVerifyTx(): TransactionParts {
|
||||
val parts = receive<Any>(otherSide).unwrap {
|
||||
val parts = otherSideSession.receive<Any>().unwrap {
|
||||
when (it) {
|
||||
is FilteredTransaction -> {
|
||||
it.verify()
|
||||
|
@ -1,7 +1,7 @@
|
||||
package net.corda.node.services.transactions
|
||||
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.flows.NotaryFlow
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.services.TimeWindowChecker
|
||||
import net.corda.core.node.services.TrustedAuthorityNotaryService
|
||||
import net.corda.node.services.api.ServiceHubInternal
|
||||
@ -16,7 +16,7 @@ class RaftNonValidatingNotaryService(override val services: ServiceHubInternal,
|
||||
override val timeWindowChecker: TimeWindowChecker = TimeWindowChecker(services.clock)
|
||||
override val uniquenessProvider: RaftUniquenessProvider = RaftUniquenessProvider(services)
|
||||
|
||||
override fun createServiceFlow(otherParty: Party): NotaryFlow.Service = NonValidatingNotaryFlow(otherParty, this)
|
||||
override fun createServiceFlow(otherPartySession: FlowSession): NotaryFlow.Service = NonValidatingNotaryFlow(otherPartySession, this)
|
||||
|
||||
override fun start() {
|
||||
uniquenessProvider.start()
|
||||
|
@ -1,7 +1,7 @@
|
||||
package net.corda.node.services.transactions
|
||||
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.flows.NotaryFlow
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.services.TimeWindowChecker
|
||||
import net.corda.core.node.services.TrustedAuthorityNotaryService
|
||||
import net.corda.node.services.api.ServiceHubInternal
|
||||
@ -16,7 +16,7 @@ class RaftValidatingNotaryService(override val services: ServiceHubInternal, ove
|
||||
override val timeWindowChecker: TimeWindowChecker = TimeWindowChecker(services.clock)
|
||||
override val uniquenessProvider: RaftUniquenessProvider = RaftUniquenessProvider(services)
|
||||
|
||||
override fun createServiceFlow(otherParty: Party): NotaryFlow.Service = ValidatingNotaryFlow(otherParty, this)
|
||||
override fun createServiceFlow(otherPartySession: FlowSession): NotaryFlow.Service = ValidatingNotaryFlow(otherPartySession, this)
|
||||
|
||||
override fun start() {
|
||||
uniquenessProvider.start()
|
||||
|
@ -1,7 +1,7 @@
|
||||
package net.corda.node.services.transactions
|
||||
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.flows.NotaryFlow
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.services.TimeWindowChecker
|
||||
import net.corda.core.node.services.TrustedAuthorityNotaryService
|
||||
import net.corda.nodeapi.ServiceType
|
||||
@ -17,7 +17,7 @@ class SimpleNotaryService(override val services: ServiceHubInternal, override va
|
||||
override val timeWindowChecker = TimeWindowChecker(services.clock)
|
||||
override val uniquenessProvider = PersistentUniquenessProvider()
|
||||
|
||||
override fun createServiceFlow(otherParty: Party): NotaryFlow.Service = NonValidatingNotaryFlow(otherParty, this)
|
||||
override fun createServiceFlow(otherPartySession: FlowSession): NotaryFlow.Service = NonValidatingNotaryFlow(otherPartySession, this)
|
||||
|
||||
override fun start() {}
|
||||
override fun stop() {}
|
||||
|
@ -3,7 +3,6 @@ package net.corda.node.services.transactions
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.TransactionVerificationException
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.services.TrustedAuthorityNotaryService
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import java.security.SignatureException
|
||||
@ -14,7 +13,7 @@ import java.security.SignatureException
|
||||
* has its input states "blocked" by a transaction from another party, and needs to establish whether that transaction was
|
||||
* indeed valid.
|
||||
*/
|
||||
class ValidatingNotaryFlow(otherSide: Party, service: TrustedAuthorityNotaryService) : NotaryFlow.Service(otherSide, service) {
|
||||
class ValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAuthorityNotaryService) : NotaryFlow.Service(otherSideSession, service) {
|
||||
/**
|
||||
* The received transaction is checked for contract-validity, which requires fully resolving it into a
|
||||
* [TransactionForVerification], for which the caller also has to to reveal the whole transaction
|
||||
@ -23,7 +22,7 @@ class ValidatingNotaryFlow(otherSide: Party, service: TrustedAuthorityNotaryServ
|
||||
@Suspendable
|
||||
override fun receiveAndVerifyTx(): TransactionParts {
|
||||
try {
|
||||
val stx = subFlow(ReceiveTransactionFlow(otherSide, checkSufficientSignatures = false))
|
||||
val stx = subFlow(ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = false))
|
||||
val notary = stx.notary
|
||||
checkNotary(notary)
|
||||
checkSignatures(stx)
|
||||
|
@ -1,7 +1,7 @@
|
||||
package net.corda.node.services.transactions
|
||||
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.flows.NotaryFlow
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.services.TimeWindowChecker
|
||||
import net.corda.core.node.services.TrustedAuthorityNotaryService
|
||||
import net.corda.nodeapi.ServiceType
|
||||
@ -17,7 +17,7 @@ class ValidatingNotaryService(override val services: ServiceHubInternal, overrid
|
||||
override val timeWindowChecker = TimeWindowChecker(services.clock)
|
||||
override val uniquenessProvider = PersistentUniquenessProvider()
|
||||
|
||||
override fun createServiceFlow(otherParty: Party): NotaryFlow.Service = ValidatingNotaryFlow(otherParty, this)
|
||||
override fun createServiceFlow(otherPartySession: FlowSession): NotaryFlow.Service = ValidatingNotaryFlow(otherPartySession, this)
|
||||
|
||||
override fun start() {}
|
||||
override fun stop() {}
|
||||
|
@ -4,7 +4,10 @@ import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.internal.copyToDirectory
|
||||
import net.corda.core.internal.createDirectories
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.list
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.unwrap
|
||||
@ -68,21 +71,22 @@ class CordappSmokeTest {
|
||||
override fun call(): Pair<FlowInfo, FlowInfo> {
|
||||
// This receive will kick off SendBackInitiatorFlowContext by sending a session-init with our app name.
|
||||
// SendBackInitiatorFlowContext will send back our context using the information from this session-init
|
||||
val sessionInitContext = receive<FlowInfo>(otherParty).unwrap { it }
|
||||
val session = initiateFlow(otherParty)
|
||||
val sessionInitContext = session.receive<FlowInfo>().unwrap { it }
|
||||
// This context is taken from the session-confirm message
|
||||
val sessionConfirmContext = getFlowInfo(otherParty)
|
||||
val sessionConfirmContext = session.getCounterpartyFlowInfo()
|
||||
return Pair(sessionInitContext, sessionConfirmContext)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@InitiatedBy(GatherContextsFlow::class)
|
||||
class SendBackInitiatorFlowContext(private val otherParty: Party) : FlowLogic<Unit>() {
|
||||
class SendBackInitiatorFlowContext(private val otherPartySession: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
// An initiated flow calling getFlowContext on its initiator will get the context from the session-init
|
||||
val sessionInitContext = getFlowInfo(otherParty)
|
||||
send(otherParty, sessionInitContext)
|
||||
val sessionInitContext = otherPartySession.getCounterpartyFlowInfo()
|
||||
otherPartySession.send(sessionInitContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,21 @@
|
||||
package net.corda.node.cordapp
|
||||
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.flows.InitiatedBy
|
||||
import net.corda.node.internal.cordapp.Cordapp
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.node.internal.cordapp.CordappLoader
|
||||
import org.junit.Assert
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Test
|
||||
import java.nio.file.Paths
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
|
||||
@InitiatingFlow
|
||||
class DummyFlow : FlowLogic<Unit>() {
|
||||
override fun call() { }
|
||||
}
|
||||
|
||||
@InitiatedBy(DummyFlow::class)
|
||||
class LoaderTestFlow : FlowLogic<Unit>() {
|
||||
class LoaderTestFlow(unusedSession: FlowSession) : FlowLogic<Unit>() {
|
||||
override fun call() { }
|
||||
}
|
||||
|
||||
|
@ -4,10 +4,7 @@ import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.InitiatedBy
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
@ -562,9 +559,10 @@ class TwoPartyTradeFlowTests {
|
||||
} else {
|
||||
ourIdentityAndCert
|
||||
}
|
||||
send(buyer, TestTx(notary, price, anonymous))
|
||||
val buyerSession = initiateFlow(buyer)
|
||||
buyerSession.send(TestTx(notary, price, anonymous))
|
||||
return subFlow(Seller(
|
||||
buyer,
|
||||
buyerSession,
|
||||
assetToSell,
|
||||
price,
|
||||
myPartyAndCert))
|
||||
@ -572,14 +570,14 @@ class TwoPartyTradeFlowTests {
|
||||
}
|
||||
|
||||
@InitiatedBy(SellerInitiator::class)
|
||||
class BuyerAcceptor(private val seller: Party) : FlowLogic<SignedTransaction>() {
|
||||
class BuyerAcceptor(private val sellerSession: FlowSession) : FlowLogic<SignedTransaction>() {
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val (notary, price, anonymous) = receive<TestTx>(seller).unwrap {
|
||||
val (notary, price, anonymous) = sellerSession.receive<TestTx>().unwrap {
|
||||
require(serviceHub.networkMapCache.isNotary(it.notaryIdentity)) { "${it.notaryIdentity} is not a notary" }
|
||||
it
|
||||
}
|
||||
return subFlow(Buyer(seller, notary, price, CommercialPaper.State::class.java, anonymous))
|
||||
return subFlow(Buyer(sellerSession, notary, price, CommercialPaper.State::class.java, anonymous))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,6 @@ import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.services.VaultQueryService
|
||||
import net.corda.core.node.services.queryBy
|
||||
@ -16,10 +15,10 @@ import net.corda.core.node.services.vault.SortAttribute
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.node.internal.StartedNode
|
||||
import net.corda.nodeapi.ServiceInfo
|
||||
import net.corda.node.services.network.NetworkMapService
|
||||
import net.corda.node.services.statemachine.StateMachineManager
|
||||
import net.corda.node.services.transactions.ValidatingNotaryService
|
||||
import net.corda.nodeapi.ServiceInfo
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
import net.corda.testing.chooseIdentity
|
||||
import net.corda.testing.contracts.DUMMY_PROGRAM_ID
|
||||
@ -50,15 +49,15 @@ class ScheduledFlowTests {
|
||||
val processed: Boolean = false,
|
||||
override val linearId: UniqueIdentifier = UniqueIdentifier()) : SchedulableState, LinearState {
|
||||
override fun nextScheduledActivity(thisStateRef: StateRef, flowLogicRefFactory: FlowLogicRefFactory): ScheduledActivity? {
|
||||
if (!processed) {
|
||||
return if (!processed) {
|
||||
val logicRef = flowLogicRefFactory.create(ScheduledFlow::class.java, thisStateRef)
|
||||
return ScheduledActivity(logicRef, creationTime)
|
||||
ScheduledActivity(logicRef, creationTime)
|
||||
} else {
|
||||
return null
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override val participants: List<AbstractParty> = listOf(source, destination)
|
||||
override val participants: List<Party> get() = listOf(source, destination)
|
||||
}
|
||||
|
||||
class InsertInitialStateFlow(private val destination: Party) : FlowLogic<Unit>() {
|
||||
@ -70,7 +69,7 @@ class ScheduledFlowTests {
|
||||
.addOutputState(scheduledState, DUMMY_PROGRAM_ID)
|
||||
.addCommand(dummyCommand(ourIdentity.owningKey))
|
||||
val tx = serviceHub.signInitialTransaction(builder)
|
||||
subFlow(FinalityFlow(tx, setOf(ourIdentity)))
|
||||
subFlow(FinalityFlow(tx))
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,7 +91,7 @@ class ScheduledFlowTests {
|
||||
.addOutputState(newStateOutput, DUMMY_PROGRAM_ID)
|
||||
.addCommand(dummyCommand(ourIdentity.owningKey))
|
||||
val tx = serviceHub.signInitialTransaction(builder)
|
||||
subFlow(FinalityFlow(tx, setOf(scheduledState.source, scheduledState.destination)))
|
||||
subFlow(FinalityFlow(tx, setOf(scheduledState.destination)))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@ package net.corda.node.services.network
|
||||
|
||||
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.InitiatingFlow
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
@ -170,17 +171,18 @@ class PersistentNetworkMapCacheTest : NodeBasedTest() {
|
||||
override fun call(): String {
|
||||
println("SEND FLOW to $otherParty")
|
||||
println("Party key ${otherParty.owningKey.toBase58String()}")
|
||||
return sendAndReceive<String>(otherParty, "Hi!").unwrap { it }
|
||||
val session = initiateFlow(otherParty)
|
||||
return session.sendAndReceive<String>("Hi!").unwrap { it }
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(SendFlow::class)
|
||||
private class SendBackFlow(val otherParty: Party) : FlowLogic<Unit>() {
|
||||
private class SendBackFlow(val otherSideSession: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
println("SEND BACK FLOW to $otherParty")
|
||||
println("Party key ${otherParty.owningKey.toBase58String()}")
|
||||
send(otherParty, "Hello!")
|
||||
println("SEND BACK FLOW to ${otherSideSession.counterparty}")
|
||||
println("Party key ${otherSideSession.counterparty.owningKey.toBase58String()}")
|
||||
otherSideSession.send("Hello!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,116 +0,0 @@
|
||||
package net.corda.node.services.persistence
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.Issued
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.InitiatedBy
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.SendTransactionFlow
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.services.queryBy
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.finance.USD
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.node.internal.StartedNode
|
||||
import net.corda.node.services.NotifyTransactionHandler
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
import net.corda.testing.MEGA_CORP
|
||||
import net.corda.testing.chooseIdentity
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
/**
|
||||
* Tests for the data vending service.
|
||||
*/
|
||||
class DataVendingServiceTests {
|
||||
lateinit var mockNet: MockNetwork
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mockNet = MockNetwork()
|
||||
}
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
mockNet.stopNodes()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `notify of transaction`() {
|
||||
val nodes = mockNet.createSomeNodes(2)
|
||||
val vaultServiceNode = nodes.partyNodes[0]
|
||||
val registerNode = nodes.partyNodes[1]
|
||||
val beneficiary = vaultServiceNode.info.chooseIdentity()
|
||||
val deposit = registerNode.info.chooseIdentity().ref(1)
|
||||
mockNet.runNetwork()
|
||||
|
||||
// Generate an issuance transaction
|
||||
val ptx = TransactionBuilder(null)
|
||||
Cash().generateIssue(ptx, Amount(100, Issued(deposit, USD)), beneficiary, DUMMY_NOTARY)
|
||||
|
||||
// Complete the cash transaction, and then manually relay it
|
||||
val tx = registerNode.services.signInitialTransaction(ptx)
|
||||
vaultServiceNode.database.transaction {
|
||||
assertThat(vaultServiceNode.services.vaultQueryService.queryBy<Cash.State>().states.isEmpty())
|
||||
|
||||
registerNode.sendNotifyTx(tx, vaultServiceNode)
|
||||
|
||||
// Check the transaction is in the receiving node
|
||||
val actual = vaultServiceNode.services.vaultQueryService.queryBy<Cash.State>().states.singleOrNull()
|
||||
val expected = tx.tx.outRef<Cash.State>(0)
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that invalid transactions are rejected.
|
||||
*/
|
||||
@Test
|
||||
fun `notify failure`() {
|
||||
val nodes = mockNet.createSomeNodes(2)
|
||||
val vaultServiceNode = nodes.partyNodes[0]
|
||||
val registerNode = nodes.partyNodes[1]
|
||||
val beneficiary = vaultServiceNode.info.chooseIdentity()
|
||||
val deposit = MEGA_CORP.ref(1)
|
||||
mockNet.runNetwork()
|
||||
|
||||
// Generate an issuance transaction
|
||||
val ptx = TransactionBuilder(DUMMY_NOTARY)
|
||||
Cash().generateIssue(ptx, Amount(100, Issued(deposit, USD)), beneficiary, DUMMY_NOTARY)
|
||||
|
||||
// The transaction tries issuing MEGA_CORP cash, but we aren't the issuer, so it's invalid
|
||||
val tx = registerNode.services.signInitialTransaction(ptx)
|
||||
vaultServiceNode.database.transaction {
|
||||
assertThat(vaultServiceNode.services.vaultQueryService.queryBy<Cash.State>().states.isEmpty())
|
||||
|
||||
registerNode.sendNotifyTx(tx, vaultServiceNode)
|
||||
|
||||
// Check the transaction is not in the receiving node
|
||||
assertThat(vaultServiceNode.services.vaultQueryService.queryBy<Cash.State>().states.isEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
private fun StartedNode<*>.sendNotifyTx(tx: SignedTransaction, walletServiceNode: StartedNode<*>) {
|
||||
walletServiceNode.internals.registerInitiatedFlow(InitiateNotifyTxFlow::class.java)
|
||||
services.startFlow(NotifyTxFlow(walletServiceNode.info.chooseIdentity(), tx))
|
||||
mockNet.runNetwork()
|
||||
}
|
||||
|
||||
@InitiatingFlow
|
||||
private class NotifyTxFlow(val otherParty: Party, val stx: SignedTransaction) : FlowLogic<Void?>() {
|
||||
@Suspendable
|
||||
override fun call() = subFlow(SendTransactionFlow(otherParty, stx))
|
||||
}
|
||||
|
||||
@InitiatedBy(NotifyTxFlow::class)
|
||||
private class InitiateNotifyTxFlow(val otherParty: Party) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() = subFlow(NotifyTransactionHandler(otherParty))
|
||||
}
|
||||
}
|
@ -37,7 +37,7 @@ class NodeSchemaServiceTest {
|
||||
/**
|
||||
* Note: this test verifies auto-scanning to register identified [MappedSchema] schemas.
|
||||
* By default, Driver uses the caller package for auto-scanning:
|
||||
* System.setProperty("net.corda.node.cordapp.scan.package", callerPackage)
|
||||
* System.setProperty("net.corda.node.cordapp.scan.packages", callerPackage)
|
||||
*/
|
||||
@Test
|
||||
fun `auto scanning of custom schemas for testing with Driver`() {
|
||||
|
@ -124,7 +124,7 @@ class FlowFrameworkTests {
|
||||
|
||||
@Test
|
||||
fun `exception while fiber suspended`() {
|
||||
node2.registerFlowFactory(ReceiveFlow::class) { SendFlow("Hello", it) }
|
||||
node2.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow("Hello", it) }
|
||||
val flow = ReceiveFlow(node2.info.chooseIdentity())
|
||||
val fiber = node1.services.startFlow(flow) as FlowStateMachineImpl
|
||||
// Before the flow runs change the suspend action to throw an exception
|
||||
@ -143,7 +143,7 @@ class FlowFrameworkTests {
|
||||
|
||||
@Test
|
||||
fun `flow restarted just after receiving payload`() {
|
||||
node2.registerFlowFactory(SendFlow::class) { ReceiveFlow(it).nonTerminating() }
|
||||
node2.registerFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() }
|
||||
node1.services.startFlow(SendFlow("Hello", node2.info.chooseIdentity()))
|
||||
|
||||
// We push through just enough messages to get only the payload sent
|
||||
@ -152,7 +152,7 @@ class FlowFrameworkTests {
|
||||
node2.internals.acceptableLiveFiberCountOnStop = 1
|
||||
node2.dispose()
|
||||
mockNet.runNetwork()
|
||||
val restoredFlow = node2.restartAndGetRestoredFlow<ReceiveFlow>(node1)
|
||||
val restoredFlow = node2.restartAndGetRestoredFlow<InitiatedReceiveFlow>(node1)
|
||||
assertThat(restoredFlow.receivedPayloads[0]).isEqualTo("Hello")
|
||||
}
|
||||
|
||||
@ -195,7 +195,7 @@ class FlowFrameworkTests {
|
||||
|
||||
@Test
|
||||
fun `flow loaded from checkpoint will respond to messages from before start`() {
|
||||
node1.registerFlowFactory(ReceiveFlow::class) { SendFlow("Hello", it) }
|
||||
node1.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow("Hello", it) }
|
||||
node2.services.startFlow(ReceiveFlow(node1.info.chooseIdentity()).nonTerminating()) // Prepare checkpointed receive flow
|
||||
// Make sure the add() has finished initial processing.
|
||||
node2.smm.executor.flush()
|
||||
@ -260,13 +260,13 @@ class FlowFrameworkTests {
|
||||
fun `sending to multiple parties`() {
|
||||
val node3 = mockNet.createNode(node1.network.myAddress)
|
||||
mockNet.runNetwork()
|
||||
node2.registerFlowFactory(SendFlow::class) { ReceiveFlow(it).nonTerminating() }
|
||||
node3.registerFlowFactory(SendFlow::class) { ReceiveFlow(it).nonTerminating() }
|
||||
node2.registerFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() }
|
||||
node3.registerFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() }
|
||||
val payload = "Hello World"
|
||||
node1.services.startFlow(SendFlow(payload, node2.info.chooseIdentity(), node3.info.chooseIdentity()))
|
||||
mockNet.runNetwork()
|
||||
val node2Flow = node2.getSingleFlow<ReceiveFlow>().first
|
||||
val node3Flow = node3.getSingleFlow<ReceiveFlow>().first
|
||||
val node2Flow = node2.getSingleFlow<InitiatedReceiveFlow>().first
|
||||
val node3Flow = node3.getSingleFlow<InitiatedReceiveFlow>().first
|
||||
assertThat(node2Flow.receivedPayloads[0]).isEqualTo(payload)
|
||||
assertThat(node3Flow.receivedPayloads[0]).isEqualTo(payload)
|
||||
|
||||
@ -294,8 +294,8 @@ class FlowFrameworkTests {
|
||||
mockNet.runNetwork()
|
||||
val node2Payload = "Test 1"
|
||||
val node3Payload = "Test 2"
|
||||
node2.registerFlowFactory(ReceiveFlow::class) { SendFlow(node2Payload, it) }
|
||||
node3.registerFlowFactory(ReceiveFlow::class) { SendFlow(node3Payload, it) }
|
||||
node2.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(node2Payload, it) }
|
||||
node3.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(node3Payload, it) }
|
||||
val multiReceiveFlow = ReceiveFlow(node2.info.chooseIdentity(), node3.info.chooseIdentity()).nonTerminating()
|
||||
node1.services.startFlow(multiReceiveFlow)
|
||||
node1.internals.acceptableLiveFiberCountOnStop = 1
|
||||
@ -420,10 +420,11 @@ class FlowFrameworkTests {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
// Kick off the flow on the other side ...
|
||||
send(otherParty, 1)
|
||||
val session = initiateFlow(otherParty)
|
||||
session.send(1)
|
||||
// ... then pause this one until it's received the session-end message from the other side
|
||||
receivedOtherFlowEnd.acquire()
|
||||
sendAndReceive<Int>(otherParty, 2)
|
||||
session.sendAndReceive<Int>(2)
|
||||
}
|
||||
}
|
||||
|
||||
@ -543,14 +544,14 @@ class FlowFrameworkTests {
|
||||
)
|
||||
}
|
||||
|
||||
private class ConditionalExceptionFlow(val otherParty: Party, val sendPayload: Any) : FlowLogic<Unit>() {
|
||||
private class ConditionalExceptionFlow(val otherPartySession: FlowSession, val sendPayload: Any) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val throwException = receive<Boolean>(otherParty).unwrap { it }
|
||||
val throwException = otherPartySession.receive<Boolean>().unwrap { it }
|
||||
if (throwException) {
|
||||
throw MyFlowException("Throwing exception as requested")
|
||||
}
|
||||
send(otherParty, sendPayload)
|
||||
otherPartySession.send(sendPayload)
|
||||
}
|
||||
}
|
||||
|
||||
@ -559,7 +560,7 @@ class FlowFrameworkTests {
|
||||
@InitiatingFlow
|
||||
class AskForExceptionFlow(val otherParty: Party, val throwException: Boolean) : FlowLogic<String>() {
|
||||
@Suspendable
|
||||
override fun call(): String = sendAndReceive<String>(otherParty, throwException).unwrap { it }
|
||||
override fun call(): String = initiateFlow(otherParty).sendAndReceive<String>(throwException).unwrap { it }
|
||||
}
|
||||
|
||||
class RetryOnExceptionFlow(val otherParty: Party) : FlowLogic<String>() {
|
||||
@ -581,7 +582,7 @@ class FlowFrameworkTests {
|
||||
|
||||
@Test
|
||||
fun `serialisation issue in counterparty`() {
|
||||
node2.registerFlowFactory(ReceiveFlow::class) { SendFlow(NonSerialisableData(1), it) }
|
||||
node2.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(NonSerialisableData(1), it) }
|
||||
val result = node1.services.startFlow(ReceiveFlow(node2.info.chooseIdentity())).resultFuture
|
||||
mockNet.runNetwork()
|
||||
assertThatExceptionOfType(UnexpectedFlowEndException::class.java).isThrownBy {
|
||||
@ -651,7 +652,7 @@ class FlowFrameworkTests {
|
||||
|
||||
@Test
|
||||
fun `customised client flow`() {
|
||||
val receiveFlowFuture = node2.registerFlowFactory(SendFlow::class) { ReceiveFlow(it) }
|
||||
val receiveFlowFuture = node2.registerFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it) }
|
||||
node1.services.startFlow(CustomSendFlow("Hello", node2.info.chooseIdentity())).resultFuture
|
||||
mockNet.runNetwork()
|
||||
assertThat(receiveFlowFuture.getOrThrow().receivedPayloads).containsOnly("Hello")
|
||||
@ -668,7 +669,7 @@ class FlowFrameworkTests {
|
||||
|
||||
@Test
|
||||
fun `upgraded initiating flow`() {
|
||||
node2.registerFlowFactory(UpgradedFlow::class, initiatedFlowVersion = 1) { SendFlow("Old initiated", it) }
|
||||
node2.registerFlowFactory(UpgradedFlow::class, initiatedFlowVersion = 1) { InitiatedSendFlow("Old initiated", it) }
|
||||
val result = node1.services.startFlow(UpgradedFlow(node2.info.chooseIdentity())).resultFuture
|
||||
mockNet.runNetwork()
|
||||
assertThat(receivedSessionMessages).startsWith(
|
||||
@ -684,13 +685,13 @@ class FlowFrameworkTests {
|
||||
fun `upgraded initiated flow`() {
|
||||
node2.registerFlowFactory(SendFlow::class, initiatedFlowVersion = 2) { UpgradedFlow(it) }
|
||||
val initiatingFlow = SendFlow("Old initiating", node2.info.chooseIdentity())
|
||||
node1.services.startFlow(initiatingFlow)
|
||||
val flowInfo = node1.services.startFlow(initiatingFlow).resultFuture
|
||||
mockNet.runNetwork()
|
||||
assertThat(receivedSessionMessages).startsWith(
|
||||
node1 sent sessionInit(SendFlow::class, flowVersion = 1, payload = "Old initiating") to node2,
|
||||
node2 sent sessionConfirm(flowVersion = 2) to node1
|
||||
)
|
||||
assertThat(initiatingFlow.getFlowInfo(node2.info.chooseIdentity()).flowVersion).isEqualTo(2)
|
||||
assertThat(flowInfo.get().flowVersion).isEqualTo(2)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -736,6 +737,23 @@ class FlowFrameworkTests {
|
||||
assertThat(result.getOrThrow()).isEqualTo("HelloHello")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `double initiateFlow throws`() {
|
||||
val future = node1.services.startFlow(DoubleInitiatingFlow()).resultFuture
|
||||
mockNet.runNetwork()
|
||||
assertThatExceptionOfType(IllegalStateException::class.java)
|
||||
.isThrownBy { future.getOrThrow() }
|
||||
.withMessageContaining("Attempted to initiateFlow() twice")
|
||||
}
|
||||
|
||||
@InitiatingFlow
|
||||
private class DoubleInitiatingFlow : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
initiateFlow(serviceHub.myInfo.chooseIdentity())
|
||||
initiateFlow(serviceHub.myInfo.chooseIdentity())
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//region Helpers
|
||||
@ -754,16 +772,7 @@ class FlowFrameworkTests {
|
||||
return smm.findStateMachines(P::class.java).single()
|
||||
}
|
||||
|
||||
@Deprecated("Use registerFlowFactoryExpectingFlowSession() instead")
|
||||
private inline fun <reified P : FlowLogic<*>> StartedNode<*>.registerFlowFactory(
|
||||
initiatingFlowClass: KClass<out FlowLogic<*>>,
|
||||
initiatedFlowVersion: Int = 1,
|
||||
noinline flowFactory: (Party) -> P): CordaFuture<P>
|
||||
{
|
||||
return registerFlowFactoryExpectingFlowSession(initiatingFlowClass, initiatedFlowVersion, { flowFactory(it.counterparty) })
|
||||
}
|
||||
|
||||
private inline fun <reified P : FlowLogic<*>> StartedNode<*>.registerFlowFactoryExpectingFlowSession(
|
||||
initiatingFlowClass: KClass<out FlowLogic<*>>,
|
||||
initiatedFlowVersion: Int = 1,
|
||||
noinline flowFactory: (FlowSession) -> P): CordaFuture<P>
|
||||
@ -858,13 +867,25 @@ class FlowFrameworkTests {
|
||||
}
|
||||
|
||||
@InitiatingFlow
|
||||
private open class SendFlow(val payload: Any, vararg val otherParties: Party) : FlowLogic<Unit>() {
|
||||
private open class SendFlow(val payload: Any, vararg val otherParties: Party) : FlowLogic<FlowInfo>() {
|
||||
init {
|
||||
require(otherParties.isNotEmpty())
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call() = otherParties.forEach { send(it, payload) }
|
||||
override fun call(): FlowInfo {
|
||||
val flowInfos = otherParties.map {
|
||||
val session = initiateFlow(it)
|
||||
session.send(payload)
|
||||
session.getCounterpartyFlowInfo()
|
||||
}.toList()
|
||||
return flowInfos.first()
|
||||
}
|
||||
}
|
||||
|
||||
private open class InitiatedSendFlow(val payload: Any, val otherPartySession: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() = otherPartySession.send(payload)
|
||||
}
|
||||
|
||||
private interface CustomInterface
|
||||
@ -890,7 +911,7 @@ class FlowFrameworkTests {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
progressTracker.currentStep = START_STEP
|
||||
receivedPayloads = otherParties.map { receive<String>(it).unwrap { it } }
|
||||
receivedPayloads = otherParties.map { initiateFlow(it).receive<String>().unwrap { it } }
|
||||
progressTracker.currentStep = RECEIVED_STEP
|
||||
if (nonTerminating) {
|
||||
Fiber.park()
|
||||
@ -903,26 +924,54 @@ class FlowFrameworkTests {
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatingFlow
|
||||
private class SendAndReceiveFlow(val otherParty: Party, val payload: Any) : FlowLogic<Any>() {
|
||||
@Suspendable
|
||||
override fun call(): Any = sendAndReceive<Any>(otherParty, payload).unwrap { it }
|
||||
}
|
||||
private class InitiatedReceiveFlow(val otherPartySession: FlowSession) : FlowLogic<Unit>() {
|
||||
object START_STEP : ProgressTracker.Step("Starting")
|
||||
object RECEIVED_STEP : ProgressTracker.Step("Received")
|
||||
|
||||
override val progressTracker: ProgressTracker = ProgressTracker(START_STEP, RECEIVED_STEP)
|
||||
private var nonTerminating: Boolean = false
|
||||
@Transient
|
||||
var receivedPayloads: List<String> = emptyList()
|
||||
|
||||
private class InlinedSendFlow(val payload: String, val otherParty: Party) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() = send(otherParty, payload)
|
||||
override fun call() {
|
||||
progressTracker.currentStep = START_STEP
|
||||
receivedPayloads = listOf(otherPartySession.receive<String>().unwrap { it })
|
||||
progressTracker.currentStep = RECEIVED_STEP
|
||||
if (nonTerminating) {
|
||||
Fiber.park()
|
||||
}
|
||||
}
|
||||
|
||||
fun nonTerminating(): InitiatedReceiveFlow {
|
||||
nonTerminating = true
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatingFlow
|
||||
private class PingPongFlow(val otherParty: Party, val payload: Long) : FlowLogic<Unit>() {
|
||||
private class SendAndReceiveFlow(val otherParty: Party, val payload: Any, val otherPartySession: FlowSession? = null) : FlowLogic<Any>() {
|
||||
constructor(otherPartySession: FlowSession, payload: Any) : this(otherPartySession.counterparty, payload, otherPartySession)
|
||||
@Suspendable
|
||||
override fun call(): Any = (otherPartySession ?: initiateFlow(otherParty)).sendAndReceive<Any>(payload).unwrap { it }
|
||||
}
|
||||
|
||||
private class InlinedSendFlow(val payload: String, val otherPartySession: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() = otherPartySession.send(payload)
|
||||
}
|
||||
|
||||
@InitiatingFlow
|
||||
private class PingPongFlow(val otherParty: Party, val payload: Long, val otherPartySession: FlowSession? = null) : FlowLogic<Unit>() {
|
||||
constructor(otherPartySession: FlowSession, payload: Long) : this(otherPartySession.counterparty, payload, otherPartySession)
|
||||
@Transient var receivedPayload: Long? = null
|
||||
@Transient var receivedPayload2: Long? = null
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
receivedPayload = sendAndReceive<Long>(otherParty, payload).unwrap { it }
|
||||
receivedPayload2 = sendAndReceive<Long>(otherParty, payload + 1).unwrap { it }
|
||||
val session = otherPartySession ?: initiateFlow(otherParty)
|
||||
receivedPayload = session.sendAndReceive<Long>(payload).unwrap { it }
|
||||
receivedPayload2 = session.sendAndReceive<Long>(payload + 1).unwrap { it }
|
||||
}
|
||||
}
|
||||
|
||||
@ -950,17 +999,18 @@ class FlowFrameworkTests {
|
||||
class Waiter(val stx: SignedTransaction, val otherParty: Party) : FlowLogic<SignedTransaction>() {
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
send(otherParty, stx)
|
||||
val otherPartySession = initiateFlow(otherParty)
|
||||
otherPartySession.send(stx)
|
||||
return waitForLedgerCommit(stx.id)
|
||||
}
|
||||
}
|
||||
|
||||
class Committer(val otherParty: Party, val throwException: (() -> Exception)? = null) : FlowLogic<SignedTransaction>() {
|
||||
class Committer(val otherPartySession: FlowSession, val throwException: (() -> Exception)? = null) : FlowLogic<SignedTransaction>() {
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val stx = receive<SignedTransaction>(otherParty).unwrap { it }
|
||||
val stx = otherPartySession.receive<SignedTransaction>().unwrap { it }
|
||||
if (throwException != null) throw throwException.invoke()
|
||||
return subFlow(FinalityFlow(stx, setOf(otherParty))).single()
|
||||
return subFlow(FinalityFlow(stx, setOf(otherPartySession.counterparty)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -969,7 +1019,8 @@ class FlowFrameworkTests {
|
||||
private class VaultQueryFlow(val stx: SignedTransaction, val otherParty: Party) : FlowLogic<List<StateAndRef<ContractState>>>() {
|
||||
@Suspendable
|
||||
override fun call(): List<StateAndRef<ContractState>> {
|
||||
send(otherParty, stx)
|
||||
val otherPartySession = initiateFlow(otherParty)
|
||||
otherPartySession.send(stx)
|
||||
// hold onto reference here to force checkpoint of vaultQueryService and thus
|
||||
// prove it is registered as a tokenizableService in the node
|
||||
val vaultQuerySvc = serviceHub.vaultQueryService
|
||||
@ -979,27 +1030,29 @@ class FlowFrameworkTests {
|
||||
}
|
||||
|
||||
@InitiatingFlow(version = 2)
|
||||
private class UpgradedFlow(val otherParty: Party) : FlowLogic<Pair<Any, Int>>() {
|
||||
private class UpgradedFlow(val otherParty: Party, val otherPartySession: FlowSession? = null) : FlowLogic<Pair<Any, Int>>() {
|
||||
constructor(otherPartySession: FlowSession) : this(otherPartySession.counterparty, otherPartySession)
|
||||
@Suspendable
|
||||
override fun call(): Pair<Any, Int> {
|
||||
val received = receive<Any>(otherParty).unwrap { it }
|
||||
val otherFlowVersion = getFlowInfo(otherParty).flowVersion
|
||||
val otherPartySession = this.otherPartySession ?: initiateFlow(otherParty)
|
||||
val received = otherPartySession.receive<Any>().unwrap { it }
|
||||
val otherFlowVersion = otherPartySession.getCounterpartyFlowInfo().flowVersion
|
||||
return Pair(received, otherFlowVersion)
|
||||
}
|
||||
}
|
||||
|
||||
private class SingleInlinedSubFlow(val otherParty: Party) : FlowLogic<Unit>() {
|
||||
private class SingleInlinedSubFlow(val otherPartySession: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val payload = receive<String>(otherParty).unwrap { it }
|
||||
subFlow(InlinedSendFlow(payload + payload, otherParty))
|
||||
val payload = otherPartySession.receive<String>().unwrap { it }
|
||||
subFlow(InlinedSendFlow(payload + payload, otherPartySession))
|
||||
}
|
||||
}
|
||||
|
||||
private class DoubleInlinedSubFlow(val otherParty: Party) : FlowLogic<Unit>() {
|
||||
private class DoubleInlinedSubFlow(val otherPartySession: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
subFlow(SingleInlinedSubFlow(otherParty))
|
||||
subFlow(SingleInlinedSubFlow(otherPartySession))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -106,7 +106,9 @@ private fun sender(rpc: CordaRPCOps, inputStream: InputStream, hash: SecureHash.
|
||||
}
|
||||
|
||||
@StartableByRPC
|
||||
class AttachmentDemoFlow(val otherSide: Party, val notary: Party, val hash: SecureHash.SHA256) : FlowLogic<SignedTransaction>() {
|
||||
class AttachmentDemoFlow(private val otherSide: Party,
|
||||
private val notary: Party,
|
||||
private val attachId: SecureHash.SHA256) : FlowLogic<SignedTransaction>() {
|
||||
|
||||
object SIGNING : ProgressTracker.Step("Signing transaction")
|
||||
|
||||
@ -116,16 +118,16 @@ class AttachmentDemoFlow(val otherSide: Party, val notary: Party, val hash: Secu
|
||||
override fun call(): SignedTransaction {
|
||||
// Create a trivial transaction with an output that describes the attachment, and the attachment itself
|
||||
val ptx = TransactionBuilder(notary)
|
||||
.addOutputState(AttachmentContract.State(hash), ATTACHMENT_PROGRAM_ID)
|
||||
.addOutputState(AttachmentContract.State(attachId), ATTACHMENT_PROGRAM_ID)
|
||||
.addCommand(AttachmentContract.Command, ourIdentity.owningKey)
|
||||
.addAttachment(hash)
|
||||
.addAttachment(attachId)
|
||||
|
||||
progressTracker.currentStep = SIGNING
|
||||
|
||||
// Send the transaction to the other recipient
|
||||
val stx = serviceHub.signInitialTransaction(ptx)
|
||||
|
||||
return subFlow(FinalityFlow(stx, setOf(otherSide))).single()
|
||||
return subFlow(FinalityFlow(stx, setOf(otherSide)))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,7 @@ import kotlin.test.assertTrue
|
||||
class BankOfCordaHttpAPITest {
|
||||
@Test
|
||||
fun `issuer flow via Http`() {
|
||||
driver(dsl = {
|
||||
driver(extraCordappPackagesToScan = listOf("net.corda.finance"), dsl = {
|
||||
val bigCorpNodeFuture = startNode(providedName = BIGCORP_LEGAL_NAME)
|
||||
val nodeBankOfCordaFuture = startNode(providedName = BOC.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
|
||||
val (nodeBankOfCorda) = listOf(nodeBankOfCordaFuture, bigCorpNodeFuture).map { it.getOrThrow() }
|
||||
|
@ -18,7 +18,7 @@ import org.junit.Test
|
||||
class BankOfCordaRPCClientTest {
|
||||
@Test
|
||||
fun `issuer flow via RPC`() {
|
||||
driver(dsl = {
|
||||
driver(extraCordappPackagesToScan = listOf("net.corda.finance"), dsl = {
|
||||
val bocManager = User("bocManager", "password1", permissions = setOf(
|
||||
startFlowPermission<CashIssueAndPaymentFlow>()))
|
||||
val bigCorpCFO = User("bigCorpCFO", "password2", permissions = emptySet())
|
||||
|
@ -4,11 +4,7 @@ import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.Command
|
||||
import net.corda.core.crypto.MerkleTreeException
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.InitiatedBy
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.internal.ThreadBox
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.services.CordaService
|
||||
@ -46,17 +42,17 @@ import kotlin.collections.set
|
||||
object NodeInterestRates {
|
||||
// DOCSTART 2
|
||||
@InitiatedBy(RatesFixFlow.FixSignFlow::class)
|
||||
class FixSignHandler(private val otherParty: Party) : FlowLogic<Unit>() {
|
||||
class FixSignHandler(private val otherPartySession: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val request = receive<RatesFixFlow.SignRequest>(otherParty).unwrap { it }
|
||||
val request = otherPartySession.receive<RatesFixFlow.SignRequest>().unwrap { it }
|
||||
val oracle = serviceHub.cordaService(Oracle::class.java)
|
||||
send(otherParty, oracle.sign(request.ftx))
|
||||
otherPartySession.send(oracle.sign(request.ftx))
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(RatesFixFlow.FixQueryFlow::class)
|
||||
class FixQueryHandler(private val otherParty: Party) : FlowLogic<Unit>() {
|
||||
class FixQueryHandler(private val otherPartySession: FlowSession) : FlowLogic<Unit>() {
|
||||
object RECEIVED : ProgressTracker.Step("Received fix request")
|
||||
object SENDING : ProgressTracker.Step("Sending fix response")
|
||||
|
||||
@ -64,12 +60,12 @@ object NodeInterestRates {
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val request = receive<RatesFixFlow.QueryRequest>(otherParty).unwrap { it }
|
||||
val request = otherPartySession.receive<RatesFixFlow.QueryRequest>().unwrap { it }
|
||||
progressTracker.currentStep = RECEIVED
|
||||
val oracle = serviceHub.cordaService(Oracle::class.java)
|
||||
val answers = oracle.query(request.queries)
|
||||
progressTracker.currentStep = SENDING
|
||||
send(otherParty, answers)
|
||||
otherPartySession.send(answers)
|
||||
}
|
||||
}
|
||||
// DOCEND 2
|
||||
|
@ -1,12 +1,7 @@
|
||||
package net.corda.irs.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.InitiatedBy
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.finance.contracts.DealState
|
||||
@ -50,22 +45,19 @@ object AutoOfferFlow {
|
||||
require(serviceHub.networkMapCache.notaryIdentities.isNotEmpty()) { "No notary nodes registered" }
|
||||
val notary = serviceHub.networkMapCache.notaryIdentities.first() // TODO We should pass the notary as a parameter to the flow, not leave it to random choice.
|
||||
// need to pick which ever party is not us
|
||||
val otherParty = notUs(dealToBeOffered.participants).map { serviceHub.identityService.partyFromAnonymous(it) }.requireNoNulls().single()
|
||||
val otherParty = serviceHub.excludeMe(serviceHub.groupAbstractPartyByWellKnownParty(dealToBeOffered.participants)).keys.single()
|
||||
progressTracker.currentStep = DEALING
|
||||
val session = initiateFlow(otherParty)
|
||||
val instigator = Instigator(
|
||||
otherParty,
|
||||
session,
|
||||
AutoOffer(notary, dealToBeOffered),
|
||||
progressTracker.getChildProgressTracker(DEALING)!!
|
||||
)
|
||||
val stx = subFlow(instigator)
|
||||
return stx
|
||||
}
|
||||
|
||||
private fun <T : AbstractParty> notUs(parties: List<T>): List<T> {
|
||||
return parties.filter { ourIdentity != it }
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(Requester::class)
|
||||
class AutoOfferAcceptor(otherParty: Party) : Acceptor(otherParty)
|
||||
class AutoOfferAcceptor(otherSideSession: FlowSession) : Acceptor(otherSideSession)
|
||||
}
|
||||
|
@ -3,10 +3,7 @@ package net.corda.irs.flows
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.InitiatedBy
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.SchedulableFlow
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
@ -28,7 +25,7 @@ object FixingFlow {
|
||||
* who does what in the flow.
|
||||
*/
|
||||
@InitiatedBy(FixingRoleDecider::class)
|
||||
class Fixer(override val otherParty: Party) : TwoPartyDealFlow.Secondary<FixingSession>() {
|
||||
class Fixer(override val otherSideSession: FlowSession) : TwoPartyDealFlow.Secondary<FixingSession>() {
|
||||
|
||||
private lateinit var txState: TransactionState<*>
|
||||
private lateinit var deal: FixableDealState
|
||||
@ -91,7 +88,7 @@ object FixingFlow {
|
||||
* is just the "side" of the flow run by the party with the floating leg as a way of deciding who
|
||||
* does what in the flow.
|
||||
*/
|
||||
class Floater(override val otherParty: Party,
|
||||
class Floater(override val otherSideSession: FlowSession,
|
||||
override val payload: FixingSession,
|
||||
override val progressTracker: ProgressTracker = TwoPartyDealFlow.Primary.tracker()) : TwoPartyDealFlow.Primary() {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@ -141,7 +138,8 @@ object FixingFlow {
|
||||
val fixing = FixingSession(ref, fixableDeal.oracle)
|
||||
val counterparty = serviceHub.identityService.partyFromAnonymous(parties[1]) ?: throw IllegalStateException("Cannot resolve floater party")
|
||||
// Start the Floater which will then kick-off the Fixer
|
||||
subFlow(Floater(counterparty, fixing))
|
||||
val session = initiateFlow(counterparty)
|
||||
subFlow(Floater(session, fixing))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.crypto.isFulfilledBy
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
@ -97,8 +98,9 @@ open class RatesFixFlow(protected val tx: TransactionBuilder,
|
||||
class FixQueryFlow(val fixOf: FixOf, val oracle: Party) : FlowLogic<Fix>() {
|
||||
@Suspendable
|
||||
override fun call(): Fix {
|
||||
val oracleSession = initiateFlow(oracle)
|
||||
// TODO: add deadline to receive
|
||||
val resp = sendAndReceive<ArrayList<Fix>>(oracle, QueryRequest(listOf(fixOf)))
|
||||
val resp = oracleSession.sendAndReceive<ArrayList<Fix>>(QueryRequest(listOf(fixOf)))
|
||||
|
||||
return resp.unwrap {
|
||||
val fix = it.first()
|
||||
@ -114,9 +116,10 @@ open class RatesFixFlow(protected val tx: TransactionBuilder,
|
||||
val partialMerkleTx: FilteredTransaction) : FlowLogic<TransactionSignature>() {
|
||||
@Suspendable
|
||||
override fun call(): TransactionSignature {
|
||||
val resp = sendAndReceive<TransactionSignature>(oracle, SignRequest(partialMerkleTx))
|
||||
val oracleSession = initiateFlow(oracle)
|
||||
val resp = oracleSession.sendAndReceive<TransactionSignature>(SignRequest(partialMerkleTx))
|
||||
return resp.unwrap { sig ->
|
||||
check(oracle.owningKey.isFulfilledBy(listOf(sig.by)))
|
||||
check(oracleSession.counterparty.owningKey.isFulfilledBy(listOf(sig.by)))
|
||||
tx.toWireTransaction().checkSignature(sig)
|
||||
sig
|
||||
}
|
||||
|
@ -6,6 +6,8 @@ import net.corda.core.flows.InitiatedBy
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.unwrap
|
||||
@ -21,10 +23,10 @@ object UpdateBusinessDayFlow {
|
||||
data class UpdateBusinessDayMessage(val date: LocalDate)
|
||||
|
||||
@InitiatedBy(Broadcast::class)
|
||||
private class UpdateBusinessDayHandler(val otherParty: Party) : FlowLogic<Unit>() {
|
||||
private class UpdateBusinessDayHandler(val otherPartySession: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val message = receive<UpdateBusinessDayMessage>(otherParty).unwrap { it }
|
||||
val message = otherPartySession.receive<UpdateBusinessDayMessage>().unwrap { it }
|
||||
(serviceHub.clock as TestClock).updateDate(message.date)
|
||||
}
|
||||
}
|
||||
@ -63,7 +65,7 @@ object UpdateBusinessDayFlow {
|
||||
|
||||
@Suspendable
|
||||
private fun doNextRecipient(recipient: Party) {
|
||||
send(recipient, UpdateBusinessDayMessage(date))
|
||||
initiateFlow(recipient).send(UpdateBusinessDayMessage(date))
|
||||
}
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import net.corda.client.jackson.JacksonSupport
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.flows.InitiatedBy
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.identity.Party
|
||||
@ -143,11 +144,14 @@ class IRSSimulation(networkSendManuallyPumped: Boolean, runAsync: Boolean, laten
|
||||
class StartDealFlow(val otherParty: Party,
|
||||
val payload: AutoOffer) : FlowLogic<SignedTransaction>() {
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction = subFlow(Instigator(otherParty, payload))
|
||||
override fun call(): SignedTransaction {
|
||||
val session = initiateFlow(otherParty)
|
||||
return subFlow(Instigator(session, payload))
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(StartDealFlow::class)
|
||||
class AcceptDealFlow(otherParty: Party) : Acceptor(otherParty)
|
||||
class AcceptDealFlow(otherSession: FlowSession) : Acceptor(otherSession)
|
||||
|
||||
val acceptDealFlows: Observable<AcceptDealFlow> = node2.internals.registerInitiatedFlow(AcceptDealFlow::class.java)
|
||||
|
||||
|
@ -11,7 +11,9 @@ class IRSSimulationTest {
|
||||
LogHelper.setLevel("+messages") // FIXME: Don't manipulate static state in tests.
|
||||
val sim = IRSSimulation(false, false, null)
|
||||
val future = sim.start()
|
||||
while (!future.isDone) sim.iterate()
|
||||
while (!future.isDone) {
|
||||
sim.iterate()
|
||||
}
|
||||
future.getOrThrow()
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,7 @@
|
||||
package net.corda.vega.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.InitiatedBy
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
@ -33,28 +30,29 @@ object IRSTradeFlow {
|
||||
val offer = IRSState(swap, buyer, seller)
|
||||
|
||||
logger.info("Handshake finished, sending IRS trade offer message")
|
||||
val otherPartyAgreeFlag = sendAndReceive<Boolean>(otherParty, OfferMessage(notary, offer)).unwrap { it }
|
||||
val session = initiateFlow(otherParty)
|
||||
val otherPartyAgreeFlag = session.sendAndReceive<Boolean>(OfferMessage(notary, offer)).unwrap { it }
|
||||
require(otherPartyAgreeFlag)
|
||||
|
||||
return subFlow(TwoPartyDealFlow.Instigator(
|
||||
otherParty,
|
||||
session,
|
||||
TwoPartyDealFlow.AutoOffer(notary, offer)))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@InitiatedBy(Requester::class)
|
||||
class Receiver(private val replyToParty: Party) : FlowLogic<Unit>() {
|
||||
class Receiver(private val replyToSession: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
logger.info("IRSTradeFlow receiver started")
|
||||
logger.info("Handshake finished, awaiting IRS trade offer")
|
||||
|
||||
val offer = receive<OfferMessage>(replyToParty).unwrap { it }
|
||||
val offer = replyToSession.receive<OfferMessage>().unwrap { it }
|
||||
// Automatically agree - in reality we'd vet the offer message
|
||||
require(serviceHub.networkMapCache.notaryIdentities.contains(offer.notary))
|
||||
send(replyToParty, true)
|
||||
subFlow(TwoPartyDealFlow.Acceptor(replyToParty))
|
||||
replyToSession.send(true)
|
||||
subFlow(TwoPartyDealFlow.Acceptor(replyToSession))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.flows.AbstractStateReplacementFlow.Proposal
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.services.queryBy
|
||||
import net.corda.core.node.services.vault.QueryCriteria.LinearStateQueryCriteria
|
||||
@ -57,6 +58,7 @@ object SimmFlow {
|
||||
: FlowLogic<RevisionedState<PortfolioState.Update>>() {
|
||||
constructor(otherParty: Party, valuationDate: LocalDate) : this(otherParty, valuationDate, null)
|
||||
lateinit var notary: Party
|
||||
lateinit var otherPartySession: FlowSession
|
||||
|
||||
@Suspendable
|
||||
override fun call(): RevisionedState<PortfolioState.Update> {
|
||||
@ -68,6 +70,7 @@ object SimmFlow {
|
||||
val trades = serviceHub.vaultQueryService.queryBy<IRSState>(criteria).states
|
||||
|
||||
val portfolio = Portfolio(trades, valuationDate)
|
||||
otherPartySession = initiateFlow(otherParty)
|
||||
if (existing == null) {
|
||||
agreePortfolio(portfolio)
|
||||
} else {
|
||||
@ -86,18 +89,24 @@ object SimmFlow {
|
||||
val parties = Pair(ourIdentity, otherParty)
|
||||
val portfolioState = PortfolioState(portfolio.refs, parties, valuationDate)
|
||||
|
||||
send(otherParty, OfferMessage(notary, portfolioState, existing?.ref, valuationDate))
|
||||
otherPartySession.send(OfferMessage(notary, portfolioState, existing?.ref, valuationDate))
|
||||
logger.info("Awaiting two party deal acceptor")
|
||||
subFlow(TwoPartyDealFlow.Acceptor(otherParty))
|
||||
subFlow(TwoPartyDealFlow.Acceptor(otherPartySession))
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun updatePortfolio(portfolio: Portfolio, stateAndRef: StateAndRef<PortfolioState>) {
|
||||
// Receive is a hack to ensure other side is ready
|
||||
sendAndReceive<Ack>(otherParty, OfferMessage(notary, stateAndRef.state.data, existing?.ref, valuationDate))
|
||||
otherPartySession.sendAndReceive<Ack>(OfferMessage(notary, stateAndRef.state.data, existing?.ref, valuationDate))
|
||||
logger.info("Updating portfolio")
|
||||
val update = PortfolioState.Update(portfolio = portfolio.refs)
|
||||
subFlow(StateRevisionFlow.Requester(stateAndRef, update))
|
||||
subFlow(StateRevisionFlowRequester(otherPartySession, stateAndRef, update))
|
||||
}
|
||||
|
||||
private class StateRevisionFlowRequester<T>(val session: FlowSession, stateAndRef: StateAndRef<RevisionedState<T>>, update: T) : StateRevisionFlow.Requester<T>(stateAndRef, update) {
|
||||
override fun getParticipantSessions(): List<Pair<FlowSession, List<AbstractParty>>> {
|
||||
return listOf(session to listOf(session.counterparty))
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
@ -110,7 +119,7 @@ object SimmFlow {
|
||||
require(valuer != null) { "Valuer party must be known to this node" }
|
||||
val valuation = agreeValuation(portfolio, valuationDate, valuer!!)
|
||||
val update = PortfolioState.Update(valuation = valuation)
|
||||
return subFlow(StateRevisionFlow.Requester(stateRef, update)).state.data
|
||||
return subFlow(StateRevisionFlowRequester(otherPartySession, stateRef, update)).state.data
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
@ -165,7 +174,7 @@ object SimmFlow {
|
||||
// TODO: In the real world, this would be tolerance aware for different types
|
||||
@Suspendable
|
||||
private inline fun <reified T : Any> agree(data: T): Boolean {
|
||||
val valid = receive<T>(otherParty).unwrap {
|
||||
val valid = otherPartySession.receive<T>().unwrap {
|
||||
logger.trace("Comparing --> $it")
|
||||
logger.trace("with -------> $data")
|
||||
if (it is InitialMarginTriple && data is InitialMarginTriple) {
|
||||
@ -175,7 +184,7 @@ object SimmFlow {
|
||||
}
|
||||
}
|
||||
logger.trace("valid is $valid")
|
||||
send(otherParty, valid)
|
||||
otherPartySession.send(valid)
|
||||
return valid
|
||||
}
|
||||
}
|
||||
@ -184,16 +193,16 @@ object SimmFlow {
|
||||
* Receives and validates a portfolio and comes to consensus over the portfolio initial margin using SIMM.
|
||||
*/
|
||||
@InitiatedBy(Requester::class)
|
||||
class Receiver(val replyToParty: Party) : FlowLogic<Unit>() {
|
||||
class Receiver(val replyToSession: FlowSession) : FlowLogic<Unit>() {
|
||||
lateinit var offer: OfferMessage
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val criteria = LinearStateQueryCriteria(participants = listOf(replyToParty))
|
||||
val criteria = LinearStateQueryCriteria(participants = listOf(replyToSession.counterparty))
|
||||
val trades = serviceHub.vaultQueryService.queryBy<IRSState>(criteria).states
|
||||
val portfolio = Portfolio(trades)
|
||||
logger.info("SimmFlow receiver started")
|
||||
offer = receive<OfferMessage>(replyToParty).unwrap { it }
|
||||
offer = replyToSession.receive<OfferMessage>().unwrap { it }
|
||||
if (offer.stateRef == null) {
|
||||
agreePortfolio(portfolio)
|
||||
} else {
|
||||
@ -205,8 +214,8 @@ object SimmFlow {
|
||||
|
||||
@Suspendable
|
||||
private fun agree(data: Any): Boolean {
|
||||
send(replyToParty, data)
|
||||
return receive<Boolean>(replyToParty).unwrap { it }
|
||||
replyToSession.send(data)
|
||||
return replyToSession.receive<Boolean>().unwrap { it }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -287,17 +296,17 @@ object SimmFlow {
|
||||
require(offer.dealBeingOffered.portfolio == portfolio.refs)
|
||||
|
||||
val seller = TwoPartyDealFlow.Instigator(
|
||||
replyToParty,
|
||||
replyToSession,
|
||||
TwoPartyDealFlow.AutoOffer(offer.notary, offer.dealBeingOffered))
|
||||
logger.info("Starting two party deal initiator with: ${replyToParty.name}")
|
||||
logger.info("Starting two party deal initiator with: ${replyToSession.counterparty.name}")
|
||||
return subFlow(seller)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun updatePortfolio(portfolio: Portfolio) {
|
||||
logger.info("Handshake finished, awaiting Simm update")
|
||||
send(replyToParty, Ack) // Hack to state that this party is ready.
|
||||
subFlow(object : StateRevisionFlow.Receiver<PortfolioState.Update>(replyToParty) {
|
||||
replyToSession.send(Ack) // Hack to state that this party is ready.
|
||||
subFlow(object : StateRevisionFlow.Receiver<PortfolioState.Update>(replyToSession) {
|
||||
override fun verifyProposal(stx:SignedTransaction, proposal: Proposal<PortfolioState.Update>) {
|
||||
super.verifyProposal(stx, proposal)
|
||||
if (proposal.modification.portfolio != portfolio.refs) throw StateReplacementException()
|
||||
@ -310,7 +319,7 @@ object SimmFlow {
|
||||
val portfolio = serviceHub.vaultQueryService.queryBy<IRSState>(VaultQueryCriteria(stateRefs = stateRef.state.data.portfolio)).states.toPortfolio()
|
||||
val valuer = serviceHub.identityService.partyFromAnonymous(stateRef.state.data.valuer) ?: throw IllegalStateException("Unknown valuer party ${stateRef.state.data.valuer}")
|
||||
val valuation = agreeValuation(portfolio, offer.valuationDate, valuer)
|
||||
subFlow(object : StateRevisionFlow.Receiver<PortfolioState.Update>(replyToParty) {
|
||||
subFlow(object : StateRevisionFlow.Receiver<PortfolioState.Update>(replyToSession) {
|
||||
override fun verifyProposal(stx: SignedTransaction, proposal: Proposal<PortfolioState.Update>) {
|
||||
super.verifyProposal(stx, proposal)
|
||||
if (proposal.modification.valuation != valuation) throw StateReplacementException()
|
||||
|
@ -3,8 +3,8 @@ package net.corda.vega.flows
|
||||
import net.corda.core.contracts.PrivacySalt
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.flows.AbstractStateReplacementFlow
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.flows.StateReplacementException
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.vega.contracts.RevisionedState
|
||||
@ -14,7 +14,7 @@ import net.corda.vega.contracts.RevisionedState
|
||||
* on the update between two parties.
|
||||
*/
|
||||
object StateRevisionFlow {
|
||||
class Requester<T>(curStateRef: StateAndRef<RevisionedState<T>>,
|
||||
open class Requester<T>(curStateRef: StateAndRef<RevisionedState<T>>,
|
||||
updatedData: T) : AbstractStateReplacementFlow.Instigator<RevisionedState<T>, RevisionedState<T>, T>(curStateRef, updatedData) {
|
||||
override fun assembleTx(): AbstractStateReplacementFlow.UpgradeTx {
|
||||
val state = originalState.state.data
|
||||
@ -22,16 +22,12 @@ object StateRevisionFlow {
|
||||
tx.setTimeWindow(serviceHub.clock.instant(), 30.seconds)
|
||||
val privacySalt = PrivacySalt()
|
||||
tx.setPrivacySalt(privacySalt)
|
||||
|
||||
val stx = serviceHub.signInitialTransaction(tx)
|
||||
val participantKeys = state.participants.map { it.owningKey }
|
||||
// TODO: We need a much faster way of finding our key in the transaction
|
||||
val myKey = serviceHub.keyManagementService.filterMyKeys(participantKeys).single()
|
||||
return AbstractStateReplacementFlow.UpgradeTx(stx, participantKeys, myKey)
|
||||
return AbstractStateReplacementFlow.UpgradeTx(stx)
|
||||
}
|
||||
}
|
||||
|
||||
open class Receiver<in T>(otherParty: Party) : AbstractStateReplacementFlow.Acceptor<T>(otherParty) {
|
||||
open class Receiver<in T>(initiatingSession: FlowSession) : AbstractStateReplacementFlow.Acceptor<T>(initiatingSession) {
|
||||
override fun verifyProposal(stx: SignedTransaction, proposal: AbstractStateReplacementFlow.Proposal<T>) {
|
||||
val proposedTx = stx.tx
|
||||
val state = proposal.stateRef
|
||||
|
@ -9,14 +9,10 @@ import net.corda.finance.flows.CashPaymentFlow
|
||||
import net.corda.finance.schemas.CashSchemaV1
|
||||
import net.corda.finance.schemas.CommercialPaperSchemaV1
|
||||
import net.corda.node.services.FlowPermissions.Companion.startFlowPermission
|
||||
import net.corda.nodeapi.ServiceInfo
|
||||
import net.corda.node.services.transactions.SimpleNotaryService
|
||||
import net.corda.nodeapi.ServiceInfo
|
||||
import net.corda.nodeapi.User
|
||||
import net.corda.testing.BOC
|
||||
import net.corda.testing.DUMMY_BANK_A
|
||||
import net.corda.testing.DUMMY_BANK_B
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
import net.corda.testing.chooseIdentity
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.driver.poll
|
||||
import net.corda.testing.node.NodeBasedTest
|
||||
import net.corda.traderdemo.flow.BuyerFlow
|
||||
|
@ -3,6 +3,7 @@ package net.corda.traderdemo.flow
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.flows.InitiatedBy
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.Emoji
|
||||
@ -16,7 +17,7 @@ import net.corda.traderdemo.TransactionGraphSearch
|
||||
import java.util.*
|
||||
|
||||
@InitiatedBy(SellerFlow::class)
|
||||
class BuyerFlow(val otherParty: Party) : FlowLogic<Unit>() {
|
||||
class BuyerFlow(private val otherSideSession: FlowSession) : FlowLogic<Unit>() {
|
||||
|
||||
object STARTING_BUY : ProgressTracker.Step("Seller connected, purchasing commercial paper asset")
|
||||
|
||||
@ -27,11 +28,11 @@ class BuyerFlow(val otherParty: Party) : FlowLogic<Unit>() {
|
||||
progressTracker.currentStep = STARTING_BUY
|
||||
|
||||
// Receive the offered amount and automatically agree to it (in reality this would be a longer negotiation)
|
||||
val amount = receive<Amount<Currency>>(otherParty).unwrap { it }
|
||||
val amount = otherSideSession.receive<Amount<Currency>>().unwrap { it }
|
||||
require(serviceHub.networkMapCache.notaryIdentities.isNotEmpty()) { "No notary nodes registered" }
|
||||
val notary: Party = serviceHub.networkMapCache.notaryIdentities.first()
|
||||
val buyer = TwoPartyTradeFlow.Buyer(
|
||||
otherParty,
|
||||
otherSideSession,
|
||||
notary,
|
||||
amount,
|
||||
CommercialPaper.State::class.java)
|
||||
|
@ -54,7 +54,7 @@ class CommercialPaperIssueFlow(private val amount: Amount<Currency>,
|
||||
// Sign it as ourselves.
|
||||
val stx = serviceHub.signInitialTransaction(tx)
|
||||
|
||||
subFlow(FinalityFlow(stx)).single()
|
||||
subFlow(FinalityFlow(stx))
|
||||
}
|
||||
|
||||
// Now make a dummy transaction that moves it to a new key, just to show that resolving dependencies works.
|
||||
@ -62,10 +62,9 @@ class CommercialPaperIssueFlow(private val amount: Amount<Currency>,
|
||||
val builder = TransactionBuilder(notary)
|
||||
CommercialPaper().generateMove(builder, issuance.tx.outRef(0), recipient)
|
||||
val stx = serviceHub.signInitialTransaction(builder)
|
||||
subFlow(FinalityFlow(stx)).single()
|
||||
subFlow(FinalityFlow(stx))
|
||||
}
|
||||
|
||||
return move
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -45,9 +45,10 @@ class SellerFlow(private val otherParty: Party,
|
||||
progressTracker.currentStep = TRADING
|
||||
|
||||
// Send the offered amount.
|
||||
send(otherParty, amount)
|
||||
val session = initiateFlow(otherParty)
|
||||
session.send(amount)
|
||||
val seller = TwoPartyTradeFlow.Seller(
|
||||
otherParty,
|
||||
session,
|
||||
commercialPaper,
|
||||
amount,
|
||||
cpOwner,
|
||||
|
@ -225,6 +225,7 @@ fun <A> rpcDriver(
|
||||
initialiseSerialization: Boolean = true,
|
||||
networkMapStartStrategy: NetworkMapStartStrategy = NetworkMapStartStrategy.Dedicated(startAutomatically = false),
|
||||
startNodesInProcess: Boolean = false,
|
||||
extraCordappPackagesToScan: List<String> = emptyList(),
|
||||
dsl: RPCDriverExposedDSLInterface.() -> A
|
||||
) = genericDriver(
|
||||
driverDsl = RPCDriverDSL(
|
||||
@ -236,7 +237,8 @@ fun <A> rpcDriver(
|
||||
useTestClock = useTestClock,
|
||||
networkMapStartStrategy = networkMapStartStrategy,
|
||||
isDebug = isDebug,
|
||||
startNodesInProcess = startNodesInProcess
|
||||
startNodesInProcess = startNodesInProcess,
|
||||
extraCordappPackagesToScan = extraCordappPackagesToScan
|
||||
)
|
||||
),
|
||||
coerce = { it },
|
||||
|
@ -310,6 +310,7 @@ fun <A> driver(
|
||||
initialiseSerialization: Boolean = defaultParameters.initialiseSerialization,
|
||||
networkMapStartStrategy: NetworkMapStartStrategy = defaultParameters.networkMapStartStrategy,
|
||||
startNodesInProcess: Boolean = defaultParameters.startNodesInProcess,
|
||||
extraCordappPackagesToScan: List<String> = defaultParameters.extraCordappPackagesToScan,
|
||||
dsl: DriverDSLExposedInterface.() -> A
|
||||
): A {
|
||||
return genericDriver(
|
||||
@ -319,9 +320,10 @@ fun <A> driver(
|
||||
systemProperties = systemProperties,
|
||||
driverDirectory = driverDirectory.toAbsolutePath(),
|
||||
useTestClock = useTestClock,
|
||||
isDebug = isDebug,
|
||||
networkMapStartStrategy = networkMapStartStrategy,
|
||||
startNodesInProcess = startNodesInProcess,
|
||||
isDebug = isDebug
|
||||
extraCordappPackagesToScan = extraCordappPackagesToScan
|
||||
),
|
||||
coerce = { it },
|
||||
dsl = dsl,
|
||||
@ -355,7 +357,8 @@ data class DriverParameters(
|
||||
val useTestClock: Boolean = false,
|
||||
val initialiseSerialization: Boolean = true,
|
||||
val networkMapStartStrategy: NetworkMapStartStrategy = NetworkMapStartStrategy.Dedicated(startAutomatically = true),
|
||||
val startNodesInProcess: Boolean = false
|
||||
val startNodesInProcess: Boolean = false,
|
||||
val extraCordappPackagesToScan: List<String> = emptyList()
|
||||
) {
|
||||
fun setIsDebug(isDebug: Boolean) = copy(isDebug = isDebug)
|
||||
fun setDriverDirectory(driverDirectory: Path) = copy(driverDirectory = driverDirectory)
|
||||
@ -366,6 +369,7 @@ data class DriverParameters(
|
||||
fun setInitialiseSerialization(initialiseSerialization: Boolean) = copy(initialiseSerialization = initialiseSerialization)
|
||||
fun setNetworkMapStartStrategy(networkMapStartStrategy: NetworkMapStartStrategy) = copy(networkMapStartStrategy = networkMapStartStrategy)
|
||||
fun setStartNodesInProcess(startNodesInProcess: Boolean) = copy(startNodesInProcess = startNodesInProcess)
|
||||
fun setExtraCordappPackagesToScan(extraCordappPackagesToScan: List<String>) = copy(extraCordappPackagesToScan = extraCordappPackagesToScan)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -579,14 +583,15 @@ class DriverDSL(
|
||||
val useTestClock: Boolean,
|
||||
val isDebug: Boolean,
|
||||
val networkMapStartStrategy: NetworkMapStartStrategy,
|
||||
val startNodesInProcess: Boolean
|
||||
val startNodesInProcess: Boolean,
|
||||
val extraCordappPackagesToScan: List<String>
|
||||
) : DriverDSLInternalInterface {
|
||||
private val dedicatedNetworkMapAddress = portAllocation.nextHostAndPort()
|
||||
private var _executorService: ScheduledExecutorService? = null
|
||||
val executorService get() = _executorService!!
|
||||
private var _shutdownManager: ShutdownManager? = null
|
||||
override val shutdownManager get() = _shutdownManager!!
|
||||
private val callerPackage = getCallerPackage()
|
||||
private val packagesToScanString = (extraCordappPackagesToScan + getCallerPackage()).joinToString(",")
|
||||
|
||||
class State {
|
||||
val processes = ArrayList<CordaFuture<Process>>()
|
||||
@ -786,7 +791,7 @@ class DriverDSL(
|
||||
_executorService = Executors.newScheduledThreadPool(2, ThreadFactoryBuilder().setNameFormat("driver-pool-thread-%d").build())
|
||||
_shutdownManager = ShutdownManager(executorService)
|
||||
// We set this property so that in-process nodes find cordapps. Out-of-process nodes need this passed in when started.
|
||||
System.setProperty("net.corda.node.cordapp.scan.package", callerPackage)
|
||||
System.setProperty("net.corda.node.cordapp.scan.packages", packagesToScanString)
|
||||
if (networkMapStartStrategy.startDedicated) {
|
||||
startDedicatedNetworkMapService().andForget(log) // Allow it to start concurrently with other nodes.
|
||||
}
|
||||
@ -840,7 +845,7 @@ class DriverDSL(
|
||||
}
|
||||
} else {
|
||||
val debugPort = if (isDebug) debugPortAllocation.nextPort() else null
|
||||
val processFuture = startOutOfProcessNode(executorService, nodeConfiguration, config, quasarJarPath, debugPort, systemProperties, callerPackage)
|
||||
val processFuture = startOutOfProcessNode(executorService, nodeConfiguration, config, quasarJarPath, debugPort, systemProperties, packagesToScanString)
|
||||
registerProcess(processFuture)
|
||||
return processFuture.flatMap { process ->
|
||||
val processDeathFuture = poll(executorService, "process death") {
|
||||
@ -904,7 +909,7 @@ class DriverDSL(
|
||||
quasarJarPath: String,
|
||||
debugPort: Int?,
|
||||
overriddenSystemProperties: Map<String, String>,
|
||||
callerPackage: String
|
||||
packagesToScanString: String
|
||||
): CordaFuture<Process> {
|
||||
val processFuture = executorService.fork {
|
||||
log.info("Starting out-of-process Node ${nodeConf.myLegalName.organisation}")
|
||||
@ -914,7 +919,7 @@ class DriverDSL(
|
||||
val systemProperties = overriddenSystemProperties + mapOf(
|
||||
"name" to nodeConf.myLegalName,
|
||||
"visualvm.display.name" to "corda-${nodeConf.myLegalName}",
|
||||
"net.corda.node.cordapp.scan.package" to callerPackage,
|
||||
"net.corda.node.cordapp.scan.packages" to packagesToScanString,
|
||||
"java.io.tmpdir" to System.getProperty("java.io.tmpdir") // Inherit from parent process
|
||||
)
|
||||
// See experimental/quasar-hook/README.md for how to generate.
|
||||
|
@ -76,6 +76,7 @@ fun <A> verifierDriver(
|
||||
useTestClock: Boolean = false,
|
||||
networkMapStartStrategy: NetworkMapStartStrategy = NetworkMapStartStrategy.Dedicated(startAutomatically = false),
|
||||
startNodesInProcess: Boolean = false,
|
||||
extraCordappPackagesToScan: List<String> = emptyList(),
|
||||
dsl: VerifierExposedDSLInterface.() -> A
|
||||
) = genericDriver(
|
||||
driverDsl = VerifierDriverDSL(
|
||||
@ -87,7 +88,8 @@ fun <A> verifierDriver(
|
||||
useTestClock = useTestClock,
|
||||
networkMapStartStrategy = networkMapStartStrategy,
|
||||
isDebug = isDebug,
|
||||
startNodesInProcess = startNodesInProcess
|
||||
startNodesInProcess = startNodesInProcess,
|
||||
extraCordappPackagesToScan = extraCordappPackagesToScan
|
||||
)
|
||||
),
|
||||
coerce = { it },
|
||||
|
Loading…
Reference in New Issue
Block a user