diff --git a/build.gradle b/build.gradle index 55c1ba59..da24b983 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ import static org.gradle.api.JavaVersion.VERSION_1_8 buildscript { ext { corda_release_group = 'net.corda' - corda_release_version = '4.3' + corda_release_version = '4.6-SNAPSHOT' tokens_release_group = "com.r3.corda.lib.tokens" tokens_release_version = "1.2-SNAPSHOT" corda_gradle_plugins_version = '5.0.8' @@ -13,6 +13,7 @@ buildscript { slf4j_version = '1.7.25' log4j_version = '2.9.1' jackson_version = '2.9.0' + quasar_version = '0.7.11_r3' confidential_id_release_group = "com.r3.corda.lib.ci" confidential_id_release_version = "1.0" aetherVersion = '1.0.0.v20140518' @@ -24,7 +25,10 @@ buildscript { jcenter() mavenCentral() mavenLocal() + maven { url "https://ci-artifactory.corda.r3cev.com/artifactory/corda-dependencies" } maven { url "http://ci-artifactory.corda.r3cev.com/artifactory/corda-releases" } + maven { url "https://ci-artifactory.corda.r3cev.com/artifactory/corda-dependencies-dev" } + maven { url "https://ci-artifactory.corda.r3cev.com/artifactory/corda-dev" } maven { url "https://repo.gradle.org/gradle/libs-releases-local/" } } @@ -71,9 +75,12 @@ subprojects { maven { url "http://ci-artifactory.corda.r3cev.com/artifactory/corda-lib" } maven { url "http://ci-artifactory.corda.r3cev.com/artifactory/corda-lib-dev" } maven { url "http://ci-artifactory.corda.r3cev.com/artifactory/corda-lib" } + maven { url "https://ci-artifactory.corda.r3cev.com/artifactory/corda-dependencies" } + maven { url "https://ci-artifactory.corda.r3cev.com/artifactory/corda-dependencies-dev" } maven { url "https://repo.gradle.org/gradle/libs-releases-local/" } } + apply plugin: 'kotlin' tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile) { diff --git a/contracts/build.gradle b/contracts/build.gradle index fda7af50..0e3ddf06 100644 --- a/contracts/build.gradle +++ b/contracts/build.gradle @@ -1,7 +1,7 @@ apply plugin: 'kotlin-jpa' apply plugin: 'net.corda.plugins.cordapp' -if (!(corda_release_version in ['4.1'])) { +if (!(corda_release_version in ['4.1', '4.6-SNAPSHOT'])) { apply from: "${rootProject.projectDir}/deterministic.gradle" } diff --git a/contracts/src/main/kotlin/com/r3/corda/lib/tokens/contracts/AbstractTokenContract.kt b/contracts/src/main/kotlin/com/r3/corda/lib/tokens/contracts/AbstractTokenContract.kt index a018b8e8..c9bdcb86 100644 --- a/contracts/src/main/kotlin/com/r3/corda/lib/tokens/contracts/AbstractTokenContract.kt +++ b/contracts/src/main/kotlin/com/r3/corda/lib/tokens/contracts/AbstractTokenContract.kt @@ -35,7 +35,8 @@ abstract class AbstractTokenContract : Contract { inputs: List>, outputs: List>, attachments: List, - references: List> + references: List>, + summary: List ) { // Get the JAR which implements the TokenType for this group. val jarHash: SecureHash? = verifyAllTokensUseSameTypeJar( @@ -49,7 +50,7 @@ abstract class AbstractTokenContract : Contract { // Issuances should only contain one issue command. is IssueTokenCommand -> verifyIssue(commands.single(), inputs, outputs, attachments, references) // Moves may contain more than one move command. - is MoveTokenCommand -> verifyMove(commands, inputs, outputs, attachments, references) + is MoveTokenCommand -> verifyMove(commands, inputs, outputs, attachments, references, summary) // Redeems must only contain one redeem command. is RedeemTokenCommand -> verifyRedeem(commands.single(), inputs, outputs, attachments, references) } @@ -77,7 +78,8 @@ abstract class AbstractTokenContract : Contract { inputs: List>, outputs: List>, attachments: List, - references: List> + references: List>, + summary: List ) /** @@ -120,12 +122,11 @@ abstract class AbstractTokenContract : Contract { "TokenCommand type per group! For example: You cannot map an Issue AND a Move command " + "to one group of tokens in a transaction." } - dispatchOnCommand(commands, group.inputs, group.outputs, tx.attachments, tx.references) + dispatchOnCommand(commands, group.inputs, group.outputs, tx.attachments, tx.references, tx.summary) } val allMatchedCommands = groupsAndCommands.map { it.first.first() }.toSet() - val extraCommands = (tokenCommands - allMatchedCommands).toSet() } private fun groupMatchesCommand(it: CommandWithParties, group: IndexedInOutGroup): Boolean { diff --git a/contracts/src/main/kotlin/com/r3/corda/lib/tokens/contracts/FungibleTokenContract.kt b/contracts/src/main/kotlin/com/r3/corda/lib/tokens/contracts/FungibleTokenContract.kt index 09411d8b..e3930acf 100644 --- a/contracts/src/main/kotlin/com/r3/corda/lib/tokens/contracts/FungibleTokenContract.kt +++ b/contracts/src/main/kotlin/com/r3/corda/lib/tokens/contracts/FungibleTokenContract.kt @@ -1,12 +1,19 @@ package com.r3.corda.lib.tokens.contracts +import com.r3.corda.lib.tokens.contracts.commands.IssueTokenCommand +import com.r3.corda.lib.tokens.contracts.commands.MoveTokenCommand +import com.r3.corda.lib.tokens.contracts.commands.RedeemTokenCommand import com.r3.corda.lib.tokens.contracts.commands.TokenCommand import com.r3.corda.lib.tokens.contracts.states.AbstractToken import com.r3.corda.lib.tokens.contracts.states.FungibleToken import com.r3.corda.lib.tokens.contracts.types.IssuedTokenType import com.r3.corda.lib.tokens.contracts.utilities.sumTokenStatesOrZero import net.corda.core.contracts.* +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.toStringShort import net.corda.core.internal.uncheckedCast +import net.corda.core.transactions.LedgerTransaction +import net.corda.core.contracts.DescribableContract import java.security.PublicKey /** @@ -22,7 +29,7 @@ import java.security.PublicKey * to call the super method to handle the existing commands. * 3. Add a method to handle the new command in the new sub-class contract. */ -open class FungibleTokenContract : AbstractTokenContract() { +open class FungibleTokenContract : AbstractTokenContract(), DescribableContract { override val accepts: Class get() = uncheckedCast(FungibleToken::class.java) companion object { @@ -65,8 +72,11 @@ open class FungibleTokenContract : AbstractTokenContract() { inputs: List>, outputs: List>, attachments: List, - references: List> + references: List>, + summary: List ) { + val ourSummary = describeTransaction(inputs.map{ it.state}, outputs.map { it.state }, moveCommands.map { it.value }, attachments.map { it.id }) + require(ourSummary == summary) { "The summary generated by the contract: '${ourSummary}' does not match the summary present in the tx: '${summary}'" } // Commands are grouped by Token Type, so we just need a token reference. val issuedToken: IssuedTokenType = moveCommands.first().value.token // There must be inputs and outputs present. @@ -138,4 +148,34 @@ open class FungibleTokenContract : AbstractTokenContract() { } } } + + override fun describeTransaction( + inputs: List>, + outputs: List>, + commands: List, + attachments: List): List { + return when(commands.first()) { + //verify the type jar presence and correctness + // Issuances should only contain one issue command. + is IssueTokenCommand -> listOf("") + // Moves may contain more than one move command. + is MoveTokenCommand -> constructMoveDescription(inputs, outputs) + // Redeems must only contain one redeem command. + is RedeemTokenCommand -> listOf("I AM A REDEPMPTION SONG)") + else -> emptyList() + } + } + + + private fun constructMoveDescription(inputs: List>, outputs: List>): List { + val moveFrom = inputs.first().asFungibleToken().holder.owningKey + val moveTo = (outputs.map { it.asFungibleToken().holder }).firstOrNull { p -> p.owningKey != moveFrom }?.owningKey ?: throw IllegalArgumentException("") + val amountToMove = outputs.filter { p -> p.asFungibleToken().holder.owningKey != moveFrom }.first().asFungibleToken().amount + amountToMove.displayTokenSize + amountToMove.quantity + return listOf("Move ${amountToMove.toDecimal()} ${amountToMove.token.tokenType.tokenIdentifier} from key: ${moveFrom.toStringShort()} to key: ${moveTo.toStringShort()}") + } + + private fun TransactionState.asFungibleToken() = this.data as FungibleToken + } diff --git a/contracts/src/main/kotlin/com/r3/corda/lib/tokens/contracts/NonFungibleTokenContract.kt b/contracts/src/main/kotlin/com/r3/corda/lib/tokens/contracts/NonFungibleTokenContract.kt index 848c8902..a99c83cc 100644 --- a/contracts/src/main/kotlin/com/r3/corda/lib/tokens/contracts/NonFungibleTokenContract.kt +++ b/contracts/src/main/kotlin/com/r3/corda/lib/tokens/contracts/NonFungibleTokenContract.kt @@ -2,17 +2,16 @@ package com.r3.corda.lib.tokens.contracts import com.r3.corda.lib.tokens.contracts.commands.TokenCommand import com.r3.corda.lib.tokens.contracts.states.NonFungibleToken -import net.corda.core.contracts.Attachment -import net.corda.core.contracts.CommandWithParties -import net.corda.core.contracts.ContractState -import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.* +import net.corda.core.crypto.SecureHash import net.corda.core.internal.uncheckedCast +import net.corda.core.transactions.LedgerTransaction import java.security.PublicKey /** * See kdoc for [FungibleTokenContract]. */ -class NonFungibleTokenContract : AbstractTokenContract() { +class NonFungibleTokenContract : AbstractTokenContract(), DescribableContract { override val accepts: Class get() = uncheckedCast(NonFungibleToken::class.java) @@ -47,7 +46,8 @@ class NonFungibleTokenContract : AbstractTokenContract() { inputs: List>, outputs: List>, attachments: List, - references: List> + references: List>, + summary: List ) { // There must be inputs and outputs present. require(inputs.isNotEmpty()) { "When moving a non fungible token, there must be one input state present." } @@ -90,4 +90,13 @@ class NonFungibleTokenContract : AbstractTokenContract() { "Holders of redeemed states must be the signing parties." } } + + override fun describeTransaction( + inputs: List>, + outputs: List>, + commands: List, + attachments: List + ): List { + TODO("Not yet implemented") + } } \ No newline at end of file diff --git a/workflows/build.gradle b/workflows/build.gradle index 2e669943..f0e848b8 100644 --- a/workflows/build.gradle +++ b/workflows/build.gradle @@ -3,8 +3,8 @@ apply plugin: 'net.corda.plugins.quasar-utils' apply plugin: 'net.corda.plugins.cordapp' cordapp { - targetPlatformVersion 5 - minimumPlatformVersion 5 + targetPlatformVersion 6 + minimumPlatformVersion 6 workflow { name "Token SDK Workflows" vendor "R3" @@ -45,10 +45,9 @@ dependencies { // Kotlin. compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - // Corda dependencies. - cordaCompile("$corda_release_group:corda-core:$corda_release_version") { - changing = true - } + // Corda integration dependencies + cordaCompile("$corda_release_group:corda-core:$corda_release_version") + testCompile "$corda_release_group:corda-node-driver:$corda_release_version" // Logging. testCompile "org.apache.logging.log4j:log4j-slf4j-impl:${log4j_version}" diff --git a/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/move/AbstractMoveTokensFlow.kt b/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/move/AbstractMoveTokensFlow.kt index 498c4572..170a79db 100644 --- a/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/move/AbstractMoveTokensFlow.kt +++ b/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/move/AbstractMoveTokensFlow.kt @@ -21,10 +21,13 @@ import net.corda.core.utilities.ProgressTracker * * @property participantSessions a list of flow participantSessions for the transaction participants. * @property observerSessions a list of flow participantSessions for the transaction observers. + * @property haltForExternalSigning whether to halt the flow thread while waiting for signatures if a call to an external + * service is required to obtain them, to prevent blocking other work */ abstract class AbstractMoveTokensFlow : FlowLogic() { abstract val participantSessions: List abstract val observerSessions: List + abstract val haltForExternalSigning: Boolean companion object { object GENERATE : ProgressTracker.Step("Generating tokens to move.") @@ -58,7 +61,8 @@ abstract class AbstractMoveTokensFlow : FlowLogic() { val signedTransaction = subFlow( ObserverAwareFinalityFlow( transactionBuilder = transactionBuilder, - allSessions = participantSessions + observerSessions + allSessions = participantSessions + observerSessions, + haltForExternalSigning = haltForExternalSigning ) ) progressTracker.currentStep = UPDATING diff --git a/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/move/ConfidentialMoveFungibleTokensFlow.kt b/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/move/ConfidentialMoveFungibleTokensFlow.kt index 57300226..bf57ac99 100644 --- a/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/move/ConfidentialMoveFungibleTokensFlow.kt +++ b/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/move/ConfidentialMoveFungibleTokensFlow.kt @@ -26,6 +26,8 @@ import net.corda.core.transactions.SignedTransaction * @param changeHolder holder of the change outputs, it can be confidential identity * @param observerSessions optional sessions with the observer nodes, to witch the transaction will be broadcasted * @param queryCriteria additional criteria for token selection + * @param haltForExternalSigning optional - halt the flow thread while waiting for signatures if a call to an external + * service is required to obtain them, to prevent blocking other work */ class ConfidentialMoveFungibleTokensFlow @JvmOverloads @@ -34,7 +36,8 @@ constructor( val participantSessions: List, val changeHolder: AbstractParty, val observerSessions: List = emptyList(), - val queryCriteria: QueryCriteria? = null + val queryCriteria: QueryCriteria? = null, + val haltForExternalSigning: Boolean = false ) : FlowLogic() { @JvmOverloads @@ -43,9 +46,9 @@ constructor( participantSessions: List, changeHolder: AbstractParty, queryCriteria: QueryCriteria? = null, - observerSessions: List = emptyList() - - ) : this(listOf(partyAndAmount), participantSessions, changeHolder, observerSessions, queryCriteria) + observerSessions: List = emptyList(), + haltForExternalSigning: Boolean = false + ) : this(listOf(partyAndAmount), participantSessions, changeHolder, observerSessions, queryCriteria, haltForExternalSigning) @Suspendable override fun call(): SignedTransaction { @@ -61,6 +64,6 @@ constructor( participantSessions.forEach { it.send(TransactionRole.PARTICIPANT) } observerSessions.forEach { it.send(TransactionRole.OBSERVER) } val confidentialOutputs = subFlow(ConfidentialTokensFlow(outputs, participantSessions)) - return subFlow(MoveTokensFlow(inputs, confidentialOutputs, participantSessions, observerSessions)) + return subFlow(MoveTokensFlow(inputs, confidentialOutputs, participantSessions, observerSessions, haltForExternalSigning)) } } diff --git a/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/move/ConfidentialMoveNonFungibleTokensFlow.kt b/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/move/ConfidentialMoveNonFungibleTokensFlow.kt index c5916038..34fe6fcd 100644 --- a/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/move/ConfidentialMoveNonFungibleTokensFlow.kt +++ b/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/move/ConfidentialMoveNonFungibleTokensFlow.kt @@ -22,6 +22,8 @@ import net.corda.core.transactions.SignedTransaction * @param participantSessions sessions with the participants of move transaction * @param observerSessions optional sessions with the observer nodes, to witch the transaction will be broadcasted * @param queryCriteria additional criteria for token selection + * @param haltForExternalSigning optional - halt the flow thread while waiting for signatures if a call to an external + * service is required to obtain them, to prevent blocking other work */ class ConfidentialMoveNonFungibleTokensFlow @JvmOverloads @@ -29,7 +31,8 @@ constructor( val partyAndToken: PartyAndToken, val participantSessions: List, val observerSessions: List = emptyList(), - val queryCriteria: QueryCriteria? = null + val queryCriteria: QueryCriteria? = null, + val haltForExternalSigning: Boolean = false ) : FlowLogic() { @Suspendable override fun call(): SignedTransaction { @@ -38,6 +41,6 @@ constructor( participantSessions.forEach { it.send(TransactionRole.PARTICIPANT) } observerSessions.forEach { it.send(TransactionRole.OBSERVER) } val confidentialOutput = subFlow(ConfidentialTokensFlow(listOf(output), participantSessions)).single() - return subFlow(MoveTokensFlow(input, confidentialOutput, participantSessions, observerSessions)) + return subFlow(MoveTokensFlow(listOf(input), listOf(confidentialOutput), participantSessions, observerSessions, haltForExternalSigning)) } } diff --git a/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/move/MoveFungibleTokensFlow.kt b/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/move/MoveFungibleTokensFlow.kt index d45918a6..c7dc26fb 100644 --- a/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/move/MoveFungibleTokensFlow.kt +++ b/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/move/MoveFungibleTokensFlow.kt @@ -21,6 +21,8 @@ import net.corda.core.transactions.TransactionBuilder * @param queryCriteria additional criteria for token selection * @param changeHolder optional holder of the change outputs, it can be confidential identity, if not specified it * defaults to caller's legal identity + * @param haltForExternalSigning optional - halt the flow thread while waiting for signatures if a call to an external + * service is required to obtain them, to prevent blocking other work */ class MoveFungibleTokensFlow @JvmOverloads @@ -29,7 +31,8 @@ constructor( override val participantSessions: List, override val observerSessions: List = emptyList(), val queryCriteria: QueryCriteria? = null, - val changeHolder: AbstractParty? = null + val changeHolder: AbstractParty? = null, + override val haltForExternalSigning: Boolean = false ) : AbstractMoveTokensFlow() { @JvmOverloads @@ -38,8 +41,9 @@ constructor( queryCriteria: QueryCriteria? = null, participantSessions: List, observerSessions: List = emptyList(), - changeHolder: AbstractParty? = null - ) : this(listOf(partyAndAmount), participantSessions, observerSessions, queryCriteria, changeHolder) + changeHolder: AbstractParty? = null, + haltForExternalSigning: Boolean = false + ) : this(listOf(partyAndAmount), participantSessions, observerSessions, queryCriteria, changeHolder, haltForExternalSigning) @Suspendable override fun addMove(transactionBuilder: TransactionBuilder) { diff --git a/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/move/MoveNonFungibleTokensFlow.kt b/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/move/MoveNonFungibleTokensFlow.kt index 4b4ea3ac..ff3a4897 100644 --- a/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/move/MoveNonFungibleTokensFlow.kt +++ b/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/move/MoveNonFungibleTokensFlow.kt @@ -18,6 +18,8 @@ import net.corda.core.transactions.TransactionBuilder * @param participantSessions sessions with the participants of move transaction * @param observerSessions optional sessions with the observer nodes, to witch the transaction will be broadcasted * @param queryCriteria additional criteria for token selection + * @param haltForExternalSigning optional - halt the flow thread while waiting for signatures if a call to an external + * service is required to obtain them, to prevent blocking other work */ class MoveNonFungibleTokensFlow @JvmOverloads @@ -25,7 +27,8 @@ constructor( val partyAndToken: PartyAndToken, override val participantSessions: List, override val observerSessions: List = emptyList(), - val queryCriteria: QueryCriteria? + val queryCriteria: QueryCriteria?, + override val haltForExternalSigning: Boolean = false ) : AbstractMoveTokensFlow() { @Suspendable override fun addMove(transactionBuilder: TransactionBuilder) { diff --git a/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/move/MoveTokensFlow.kt b/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/move/MoveTokensFlow.kt index 43ed0be0..1dc3aeb2 100644 --- a/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/move/MoveTokensFlow.kt +++ b/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/move/MoveTokensFlow.kt @@ -18,6 +18,8 @@ import net.corda.core.transactions.TransactionBuilder * @param outputs list of result token outputs * @param participantSessions session with the participants of move tokens transaction * @param observerSessions session with optional observers of the redeem transaction + * @param haltForExternalSigning optional - halt the flow thread while waiting for signatures if a call to an external + * service is required to obtain them, to prevent blocking other work */ class MoveTokensFlow @JvmOverloads @@ -25,15 +27,17 @@ constructor( val inputs: List>, val outputs: List, override val participantSessions: List, - override val observerSessions: List = emptyList() + override val observerSessions: List = emptyList(), + override val haltForExternalSigning: Boolean = false ) : AbstractMoveTokensFlow() { @JvmOverloads constructor( input: StateAndRef, output: AbstractToken, participantSessions: List, - observerSessions: List = emptyList() - ) : this(listOf(input), listOf(output), participantSessions, observerSessions) + observerSessions: List = emptyList(), + haltForExternalSigning: Boolean = false + ) : this(listOf(input), listOf(output), participantSessions, observerSessions, haltForExternalSigning) @Suspendable override fun addMove(transactionBuilder: TransactionBuilder) { diff --git a/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/rpc/MoveTokens.kt b/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/rpc/MoveTokens.kt index d65907b5..effe7917 100644 --- a/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/rpc/MoveTokens.kt +++ b/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/flows/rpc/MoveTokens.kt @@ -26,6 +26,8 @@ import net.corda.core.transactions.SignedTransaction * @param queryCriteria additional criteria for token selection * @param changeHolder optional holder of the change outputs, it can be confidential identity, if not specified it * defaults to caller's legal identity + * @param haltForExternalSigning optional - halt the flow thread while waiting for signatures if a call to an external + * service is required to obtain them, to prevent blocking other work */ @StartableByService @StartableByRPC @@ -36,7 +38,8 @@ constructor( val partiesAndAmounts: List>, val observers: List = emptyList(), val queryCriteria: QueryCriteria? = null, - val changeHolder: AbstractParty? = null + val changeHolder: AbstractParty? = null, + val haltForExternalSigning: Boolean = false ) : FlowLogic() { @JvmOverloads @@ -44,8 +47,9 @@ constructor( partyAndAmount: PartyAndAmount, observers: List = emptyList(), queryCriteria: QueryCriteria? = null, - changeHolder: AbstractParty? = null - ) : this(listOf(partyAndAmount), observers, queryCriteria, changeHolder) + changeHolder: AbstractParty? = null, + haltForExternalSigning: Boolean = false + ) : this(listOf(partyAndAmount), observers, queryCriteria, changeHolder, haltForExternalSigning) constructor(amount: Amount, holder: AbstractParty) : this(PartyAndAmount(holder, amount), emptyList()) @@ -59,7 +63,8 @@ constructor( participantSessions = participantSessions, observerSessions = observerSessions, queryCriteria = queryCriteria, - changeHolder = changeHolder + changeHolder = changeHolder, + haltForExternalSigning = haltForExternalSigning )) } } @@ -83,6 +88,8 @@ class MoveFungibleTokensHandler(val otherSession: FlowSession) : FlowLogic * @param partyAndToken pairing party - token that is to be moved to that party * @param observers optional observing parties to which the transaction will be broadcast * @param queryCriteria additional criteria for token selection + * @param haltForExternalSigning optional - halt the flow thread while waiting for signatures if a call to an external + * service is required to obtain them, to prevent blocking other work */ @StartableByService @StartableByRPC @@ -92,7 +99,8 @@ class MoveNonFungibleTokens constructor( val partyAndToken: PartyAndToken, val observers: List = emptyList(), - val queryCriteria: QueryCriteria? = null + val queryCriteria: QueryCriteria? = null, + val haltForExternalSigning: Boolean = false ) : FlowLogic() { @Suspendable override fun call(): SignedTransaction { @@ -102,7 +110,8 @@ constructor( partyAndToken = partyAndToken, participantSessions = participantSessions, observerSessions = observerSessions, - queryCriteria = queryCriteria + queryCriteria = queryCriteria, + haltForExternalSigning = haltForExternalSigning )) } } @@ -129,6 +138,8 @@ class MoveNonFungibleTokensHandler(val otherSession: FlowSession) : FlowLogic>, val observers: List, val queryCriteria: QueryCriteria? = null, - val changeHolder: AbstractParty? = null + val changeHolder: AbstractParty? = null, + val haltForExternalSigning: Boolean = false ) : FlowLogic() { constructor( partyAndAmount: PartyAndAmount, observers: List, queryCriteria: QueryCriteria? = null, - changeHolder: AbstractParty? = null - ) : this(listOf(partyAndAmount), observers, queryCriteria, changeHolder) + changeHolder: AbstractParty? = null, + haltForExternalSigning: Boolean = false + ) : this(listOf(partyAndAmount), observers, queryCriteria, changeHolder, haltForExternalSigning) @Suspendable override fun call(): SignedTransaction { @@ -167,7 +180,8 @@ class ConfidentialMoveFungibleTokens( participantSessions = participantSessions, observerSessions = observerSessions, queryCriteria = queryCriteria, - changeHolder = confidentialHolder + changeHolder = confidentialHolder, + haltForExternalSigning = haltForExternalSigning )) } } @@ -191,6 +205,8 @@ class ConfidentialMoveFungibleTokensHandler(val otherSession: FlowSession) : Flo * @param partyAndToken list of pairing party - token that is to be moved to that party * @param observers optional observing parties to which the transaction will be broadcast * @param queryCriteria additional criteria for token selection + * @param haltForExternalSigning optional - halt the flow thread while waiting for signatures if a call to an external + * service is required to obtain them, to prevent blocking other work */ @StartableByService @StartableByRPC @@ -198,7 +214,8 @@ class ConfidentialMoveFungibleTokensHandler(val otherSession: FlowSession) : Flo class ConfidentialMoveNonFungibleTokens( val partyAndToken: PartyAndToken, val observers: List, - val queryCriteria: QueryCriteria? = null + val queryCriteria: QueryCriteria? = null, + val haltForExternalSigning: Boolean = false ) : FlowLogic() { @Suspendable override fun call(): SignedTransaction { @@ -208,7 +225,8 @@ class ConfidentialMoveNonFungibleTokens( partyAndToken = partyAndToken, participantSessions = participantSessions, observerSessions = observerSessions, - queryCriteria = queryCriteria + queryCriteria = queryCriteria, + haltForExternalSigning = haltForExternalSigning )) } } diff --git a/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/internal/flows/finality/ObserverAwareFinalityFlow.kt b/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/internal/flows/finality/ObserverAwareFinalityFlow.kt index 8046ac6f..cfb87bc4 100644 --- a/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/internal/flows/finality/ObserverAwareFinalityFlow.kt +++ b/workflows/src/main/kotlin/com/r3/corda/lib/tokens/workflows/internal/flows/finality/ObserverAwareFinalityFlow.kt @@ -8,13 +8,22 @@ import com.r3.corda.lib.tokens.workflows.utilities.requireSessionsForParticipant import com.r3.corda.lib.tokens.workflows.utilities.toWellKnownParties import net.corda.core.contracts.CommandWithParties import net.corda.core.flows.FinalityFlow +import net.corda.core.flows.FlowExternalAsyncOperation 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.node.ServiceHub import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder +import java.security.PublicKey +import java.util.concurrent.Callable +import java.util.concurrent.CompletableFuture +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException +import java.util.function.Supplier /** * This flow is a wrapper around [FinalityFlow] and properly handles broadcasting transactions to observers (those which @@ -31,16 +40,22 @@ import net.corda.core.transactions.TransactionBuilder * transaction with observers, notice that this flow can be called either with [transactionBuilder] or * [signedTransaction] * @property allSessions a set of sessions for, at least, all the transaction participants and maybe observers + * @property haltForExternalSigning optional - halt the flow thread while waiting for signatures if a call to an external + * service is required to obtain them, to prevent blocking other work */ class ObserverAwareFinalityFlow private constructor( val allSessions: List, val signedTransaction: SignedTransaction? = null, - val transactionBuilder: TransactionBuilder? = null + val transactionBuilder: TransactionBuilder? = null, + val haltForExternalSigning: Boolean = false ) : FlowLogic() { constructor(transactionBuilder: TransactionBuilder, allSessions: List) : this(allSessions, null, transactionBuilder) + constructor(transactionBuilder: TransactionBuilder, allSessions: List, haltForExternalSigning: Boolean) + : this(allSessions, null, transactionBuilder, haltForExternalSigning) + constructor(signedTransaction: SignedTransaction, allSessions: List) : this(allSessions, signedTransaction) @@ -68,10 +83,42 @@ class ObserverAwareFinalityFlow private constructor( } // Sign and finalise the transaction, obtaining the signing keys required from the LedgerTransaction. val ourSigningKeys = ledgerTransaction.ourSigningKeys(serviceHub) - val stx = transactionBuilder?.let { - serviceHub.signInitialTransaction(it, signingPubKeys = ourSigningKeys) - } ?: signedTransaction - ?: throw IllegalArgumentException("Didn't provide transactionBuilder nor signedTransaction to the flow.") + + val stx = if (haltForExternalSigning) { + await(SignTransactionOperation(transactionBuilder!!, ourSigningKeys, serviceHub)) + } else { + transactionBuilder?.let { + serviceHub.signInitialTransaction(it, signingPubKeys = ourSigningKeys) + } ?: signedTransaction + ?: throw IllegalArgumentException("Didn't provide transactionBuilder nor signedTransaction to the flow.") + } + return subFlow(FinalityFlow(transaction = stx, sessions = finalSessions)) } + +} + +class SignTransactionOperation(private val transactionBuilder: TransactionBuilder, private val signingKeys: List, + private val serviceHub: ServiceHub) : FlowExternalAsyncOperation { + + override fun execute(deduplicationId: String) : CompletableFuture { + val executor = Executors.newFixedThreadPool(2) + return CompletableFuture.supplyAsync( + Supplier { + transactionBuilder.let { + val future = executor.submit(Callable { + serviceHub.signInitialTransaction(it, signingPubKeys = signingKeys) + }) + try { + future.get(30, TimeUnit.MINUTES) + } finally { + future.cancel(true) + executor.shutdown() + } + } + }, + executor + ) + } + } \ No newline at end of file