diff --git a/build.gradle b/build.gradle index 51e4469..00e03b1 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,15 @@ buildscript { //properties that you need to build the project mavenLocal() mavenCentral() jcenter() - maven { url "${artifactory_contextUrl}/corda-releases" } + maven { + url "https://software.r3.com/artifactory/dc-lib-dev" + credentials { + username = System.getenv('CORDA_ARTIFACTORY_USERNAME') ?: System.getProperty('corda.artifactory.username') + password = System.getenv('CORDA_ARTIFACTORY_PASSWORD') ?: System.getProperty('corda.artifactory.password') + } + } + maven { url "${artifactory_contextUrl}/corda" } + maven { url "${artifactory_contextUrl}/corda-lib" } } dependencies { @@ -14,6 +22,7 @@ buildscript { //properties that you need to build the project classpath "net.corda.plugins:cordapp:$corda_gradle_plugins_version" classpath "net.corda.plugins:cordformation:$corda_gradle_plugins_version" classpath "net.corda.plugins:quasar-utils:$corda_gradle_plugins_version" + classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlin_version" } } @@ -41,6 +50,7 @@ allprojects { //Properties that you need to compile your project (The applicatio apply from: "${rootProject.projectDir}/repositories.gradle" apply plugin: 'kotlin' + apply plugin: "kotlin-jpa" repositories { mavenLocal() @@ -49,6 +59,13 @@ allprojects { //Properties that you need to compile your project (The applicatio maven { url "${artifactory_contextUrl}/corda" } maven { url "https://jitpack.io" } maven { url "${artifactory_contextUrl}/corda-lib" } + maven { + url "https://software.r3.com/artifactory/dc-lib-dev" + credentials { + username = System.getenv('CORDA_ARTIFACTORY_USERNAME') ?: System.getProperty('corda.artifactory.username') + password = System.getenv('CORDA_ARTIFACTORY_PASSWORD') ?: System.getProperty('corda.artifactory.password') + } + } } tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile) { diff --git a/contracts/src/main/kotlin/com/r3/corda/lib/reissuance/contracts/ReissuanceLockContract.kt b/contracts/src/main/kotlin/com/r3/corda/lib/reissuance/contracts/ReissuanceLockContract.kt index fbc913d..6d64409 100644 --- a/contracts/src/main/kotlin/com/r3/corda/lib/reissuance/contracts/ReissuanceLockContract.kt +++ b/contracts/src/main/kotlin/com/r3/corda/lib/reissuance/contracts/ReissuanceLockContract.kt @@ -1,5 +1,6 @@ package com.r3.corda.lib.reissuance.contracts +import com.r3.corda.lib.reissuance.states.ReissuableState import com.r3.corda.lib.reissuance.states.ReissuanceLock import com.r3.corda.lib.reissuance.states.ReissuanceRequest import net.corda.core.contracts.* @@ -84,8 +85,17 @@ class ReissuanceLockContract: Contract where T: ContractState { reissuanceLock.status == ReissuanceLock.ReissuanceLockStatus.ACTIVE) // verify state data - "StatesAndRef objects in ReissuanceLock must be the same as re-issued states" using ( - reissuanceLock.originalStates.map { it.state.data } == otherOutputs.map { it.data }) + if (firstReissuedState.state.data is ReissuableState<*>) { + reissuanceLock.originalStates.forEachIndexed { index, stateAndRef -> + val state = stateAndRef.state.data as ReissuableState + val reissuedState = otherOutputs[index].data + "StatesAndRef objects in ReissuanceLock must be the same as re-issued states" using ( + state.isEqualForReissuance(reissuedState)) + } + } else { + "StatesAndRef objects in ReissuanceLock must be the same as re-issued states" using ( + reissuanceLock.originalStates.map { it.state.data } == otherOutputs.map { it.data }) + } // verify encumbrance reissuanceLock.originalStates.forEach { @@ -94,7 +104,6 @@ class ReissuanceLockContract: Contract where T: ContractState { otherOutputs.forEach { "Output other than ReissuanceRequest and ReissuanceLock must be encumbered" using (it.encumbrance != null) } - } } diff --git a/contracts/src/main/kotlin/com/r3/corda/lib/reissuance/states/ReissuableState.kt b/contracts/src/main/kotlin/com/r3/corda/lib/reissuance/states/ReissuableState.kt new file mode 100644 index 0000000..9d57e22 --- /dev/null +++ b/contracts/src/main/kotlin/com/r3/corda/lib/reissuance/states/ReissuableState.kt @@ -0,0 +1,37 @@ +package com.r3.corda.lib.reissuance.states + +import net.corda.core.contracts.ContractState + +/** + * The ReissuableState interface is an optional interface that states can implement if they + * need to change any of their properties during reissuance. + * + * Note: if reissued states are identical to the states being destroyed, then this interface + * is not required. + * + * For example, a token that has a counter which increments each time it is used, but needs + * to be reset upon reissuance. + * + * The methods that this interface supports are intended to: + * - Allow reissuance to create a new state to issue from an existing state [createReissuance] + * - Allow reissuance to test that a reissued state is equal to an existing state for the + * purposes of reissuance [isEqualForReissuance]. + * + */ +interface ReissuableState { + + /** + * Create a reissuable version of a state. This allows the developer to adjust fields which + * will not be the same before and after reissuance. + */ + fun createReissuance() : T + + /** + * Compare to another state of the same type, to evaluate whether for reissuance purposes + * the state is the same. This allows a developer to ignore fields which they do not expect + * to be the same before and after reissuance. + * + * @param otherState + */ + fun isEqualForReissuance(otherState : T) : Boolean +} \ No newline at end of file diff --git a/contracts/src/main/kotlin/com/r3/corda/lib/reissuance/utils/TransactionUtils.kt b/contracts/src/main/kotlin/com/r3/corda/lib/reissuance/utils/TransactionUtils.kt index fbab58f..432afb7 100644 --- a/contracts/src/main/kotlin/com/r3/corda/lib/reissuance/utils/TransactionUtils.kt +++ b/contracts/src/main/kotlin/com/r3/corda/lib/reissuance/utils/TransactionUtils.kt @@ -1,7 +1,5 @@ package com.r3.corda.lib.reissuance.utils -import net.corda.core.crypto.SecureHash -import net.corda.core.node.ServiceHub import net.corda.core.serialization.serialize import net.corda.core.transactions.SignedTransaction import java.io.ByteArrayOutputStream @@ -25,11 +23,3 @@ fun convertSignedTransactionToByteArray( return baos.toByteArray() } - -fun findSignedTransactionTrandsactionById( - serviceHub: ServiceHub, - transactionId: SecureHash -): SignedTransaction? { - return serviceHub.validatedTransactions.track().snapshot - .findLast { it.tx.id == transactionId } -} \ No newline at end of file diff --git a/dummy_workflows/src/main/kotlin/com/r3/corda/lib/reissuance/dummy_flows/dummy/simpleDummyState/CreateSimpleDummyState.kt b/dummy_workflows/src/main/kotlin/com/r3/corda/lib/reissuance/dummy_flows/dummy/simpleDummyState/CreateSimpleDummyState.kt index 2441d9d..4780e63 100644 --- a/dummy_workflows/src/main/kotlin/com/r3/corda/lib/reissuance/dummy_flows/dummy/simpleDummyState/CreateSimpleDummyState.kt +++ b/dummy_workflows/src/main/kotlin/com/r3/corda/lib/reissuance/dummy_flows/dummy/simpleDummyState/CreateSimpleDummyState.kt @@ -13,14 +13,16 @@ import net.corda.core.transactions.TransactionBuilder @InitiatingFlow @StartableByRPC class CreateSimpleDummyState( - private val owner: Party + private val owner: Party, + private val notary : Party? = null ): FlowLogic() { @Suspendable override fun call(): SecureHash { val issuer = ourIdentity val signers = listOf(issuer.owningKey) - val transactionBuilder = TransactionBuilder(notary = getPreferredNotary(serviceHub)) + val notaryToUse = notary ?: getPreferredNotary(serviceHub) + val transactionBuilder = TransactionBuilder(notary = notaryToUse) transactionBuilder.addOutputState(SimpleDummyState(owner)) transactionBuilder.addCommand(SimpleDummyStateContract.Commands.Create(), signers) diff --git a/dummy_workflows/src/main/kotlin/com/r3/corda/lib/reissuance/dummy_flows/dummy/simpleDummyState/DeleteSimpleDummyState.kt b/dummy_workflows/src/main/kotlin/com/r3/corda/lib/reissuance/dummy_flows/dummy/simpleDummyState/DeleteSimpleDummyState.kt index 683dc30..911b246 100644 --- a/dummy_workflows/src/main/kotlin/com/r3/corda/lib/reissuance/dummy_flows/dummy/simpleDummyState/DeleteSimpleDummyState.kt +++ b/dummy_workflows/src/main/kotlin/com/r3/corda/lib/reissuance/dummy_flows/dummy/simpleDummyState/DeleteSimpleDummyState.kt @@ -20,7 +20,7 @@ class DeleteSimpleDummyState( @Suspendable override fun call(): SecureHash { val signers = listOf(ourIdentity.owningKey) - val notary = getPreferredNotary(serviceHub) + val notary = originalStateAndRef.state.notary val transactionBuilder = TransactionBuilder(notary = notary) diff --git a/dummy_workflows/src/main/kotlin/com/r3/corda/lib/reissuance/dummy_flows/dummy/simpleDummyState/DeleteSimpleDummyStateAndCreateDummyStateWithInvalidEqualsMethod.kt b/dummy_workflows/src/main/kotlin/com/r3/corda/lib/reissuance/dummy_flows/dummy/simpleDummyState/DeleteSimpleDummyStateAndCreateDummyStateWithInvalidEqualsMethod.kt index d00f2fa..c1ecc47 100644 --- a/dummy_workflows/src/main/kotlin/com/r3/corda/lib/reissuance/dummy_flows/dummy/simpleDummyState/DeleteSimpleDummyStateAndCreateDummyStateWithInvalidEqualsMethod.kt +++ b/dummy_workflows/src/main/kotlin/com/r3/corda/lib/reissuance/dummy_flows/dummy/simpleDummyState/DeleteSimpleDummyStateAndCreateDummyStateWithInvalidEqualsMethod.kt @@ -22,7 +22,7 @@ class DeleteSimpleDummyStateAndCreateDummyStateWithInvalidEqualsMethod( @Suspendable override fun call(): SecureHash { val signers = listOf(ourIdentity.owningKey) - val notary = getPreferredNotary(serviceHub) + val notary = originalStateAndRef.state.notary val transactionBuilder = TransactionBuilder(notary = notary) diff --git a/dummy_workflows/src/main/kotlin/com/r3/corda/lib/reissuance/dummy_flows/dummy/simpleDummyState/UpdateSimpleDummyState.kt b/dummy_workflows/src/main/kotlin/com/r3/corda/lib/reissuance/dummy_flows/dummy/simpleDummyState/UpdateSimpleDummyState.kt index 7d1850c..4616680 100644 --- a/dummy_workflows/src/main/kotlin/com/r3/corda/lib/reissuance/dummy_flows/dummy/simpleDummyState/UpdateSimpleDummyState.kt +++ b/dummy_workflows/src/main/kotlin/com/r3/corda/lib/reissuance/dummy_flows/dummy/simpleDummyState/UpdateSimpleDummyState.kt @@ -26,7 +26,7 @@ class UpdateSimpleDummyState( val signers = setOf(owner.owningKey, newOwner.owningKey).toList() // old and new owner might be the same - val transactionBuilder = TransactionBuilder(notary = getPreferredNotary(serviceHub)) + val transactionBuilder = TransactionBuilder(notary = simpleDummyStateStateAndRef.state.notary) transactionBuilder.addInputState(simpleDummyStateStateAndRef) transactionBuilder.addOutputState(SimpleDummyState(newOwner)) transactionBuilder.addCommand(SimpleDummyStateContract.Commands.Update(), signers) diff --git a/dummy_workflows/src/test/kotlin/com/r3/corda/lib/reissuance/dummy_flows/AbstractFlowTest.kt b/dummy_workflows/src/test/kotlin/com/r3/corda/lib/reissuance/dummy_flows/AbstractFlowTest.kt index b47037a..09785cb 100644 --- a/dummy_workflows/src/test/kotlin/com/r3/corda/lib/reissuance/dummy_flows/AbstractFlowTest.kt +++ b/dummy_workflows/src/test/kotlin/com/r3/corda/lib/reissuance/dummy_flows/AbstractFlowTest.kt @@ -63,7 +63,6 @@ abstract class AbstractFlowTest { lateinit var mockNet: InternalMockNetwork - lateinit var notaryNode: TestStartedNode lateinit var issuerNode: TestStartedNode lateinit var acceptorNode: TestStartedNode lateinit var aliceNode: TestStartedNode @@ -101,10 +100,13 @@ abstract class AbstractFlowTest { lateinit var debbieLegalName: CordaX500Name lateinit var employeeLegalName: CordaX500Name - lateinit var allNotaries: List - lateinit var issuedTokenType: IssuedTokenType + val notary1Name = CordaX500Name("Notary1", "Zurich", "CH") + val notary2Name = CordaX500Name("Notary2", "Zurich", "CH") + + lateinit var notary2Party : Party + @Before fun setup() { mockNet = InternalMockNetwork( @@ -121,15 +123,19 @@ abstract class AbstractFlowTest { findCordapp("com.r3.corda.lib.reissuance.flows"), findCordapp("com.r3.corda.lib.reissuance.dummy_flows") ), - notarySpecs = listOf(MockNetworkNotarySpec(DUMMY_NOTARY_NAME, false)), + notarySpecs = listOf(notary1Name, notary2Name).map { MockNetworkNotarySpec(it) }, initialNetworkParameters = testNetworkParameters( minimumPlatformVersion = 8 // 4.6 ) ) - allNotaries = mockNet.notaryNodes - notaryNode = mockNet.notaryNodes.first() - notaryParty = notaryNode.info.singleIdentity() + notaryParty = mockNet.notaryNodes.single { + it.info.singleIdentity().name == notary1Name + }.info.singleIdentity() + + notary2Party = mockNet.notaryNodes.single { + it.info.singleIdentity().name == notary2Name + }.info.singleIdentity() } @@ -286,6 +292,16 @@ abstract class AbstractFlowTest { ) } + fun createSimpleDummyStateOnNotary( + owner: Party, + notary : Party + ): SecureHash { + return runFlow( + issuerNode, + CreateSimpleDummyState(owner, notary) + ) + } + fun createSimpleDummyStateForAccount( node: TestStartedNode, owner: AbstractParty diff --git a/dummy_workflows/src/test/kotlin/com/r3/corda/lib/reissuance/dummy_flows/DeleteReissuedStatesAndLockTest.kt b/dummy_workflows/src/test/kotlin/com/r3/corda/lib/reissuance/dummy_flows/DeleteReissuedStatesAndLockTest.kt index 32fe958..6e5fdb9 100644 --- a/dummy_workflows/src/test/kotlin/com/r3/corda/lib/reissuance/dummy_flows/DeleteReissuedStatesAndLockTest.kt +++ b/dummy_workflows/src/test/kotlin/com/r3/corda/lib/reissuance/dummy_flows/DeleteReissuedStatesAndLockTest.kt @@ -65,6 +65,34 @@ class DeleteReissuedStatesAndLockTest: AbstractFlowTest() { verifyDeletedReissuedStatesAndLock(statesToReissue) } + @Test + fun `Re-issued SimpleDummyState and corresponding ReissuanceLock are deleted on notary 2`() { + initialiseParties() + createSimpleDummyStateOnNotary(aliceParty, notary2Party) + + val statesToReissue = getStateAndRefs(aliceNode) // there is just 1 + createReissuanceRequestAndShareRequiredTransactions( + aliceNode, + statesToReissue, + SimpleDummyStateContract.Commands.Create(), + issuerParty + ) + + val reissuanceRequest = getStateAndRefs(issuerNode)[0] + reissueRequestedStates(issuerNode, reissuanceRequest, listOf()) + + val reissuedSimpleDummyStates = getStateAndRefs(aliceNode, encumbered = true) + val lockState = getStateAndRefs>(aliceNode)[0] + deleteReissuedStatesAndLock( + aliceNode, + lockState, + reissuedSimpleDummyStates, + SimpleDummyStateContract.Commands.Delete() + ) + + verifyDeletedReissuedStatesAndLock(statesToReissue) + } + @Test fun `Re-issued DummyStateRequiringAcceptance is unencumbered after the original state are deleted`() { initialiseParties() diff --git a/dummy_workflows/src/test/kotlin/com/r3/corda/lib/reissuance/dummy_flows/ReissueStatesTest.kt b/dummy_workflows/src/test/kotlin/com/r3/corda/lib/reissuance/dummy_flows/ReissueStatesTest.kt index 86d1b36..51f1d4c 100644 --- a/dummy_workflows/src/test/kotlin/com/r3/corda/lib/reissuance/dummy_flows/ReissueStatesTest.kt +++ b/dummy_workflows/src/test/kotlin/com/r3/corda/lib/reissuance/dummy_flows/ReissueStatesTest.kt @@ -60,6 +60,24 @@ class ReissueStatesTest: AbstractFlowTest() { verifyStatesAfterReissuance() } + @Test + fun `SimpleDummyState is re-issued on notary 2`() { + initialiseParties() + createSimpleDummyStateOnNotary(aliceParty, notary2Party) + + val simpleDummyState = getStateAndRefs(aliceNode)[0] + createReissuanceRequestAndShareRequiredTransactions( + aliceNode, + listOf(simpleDummyState), + SimpleDummyStateContract.Commands.Create(), + issuerParty + ) + + val reissuanceRequest = getStateAndRefs(issuerNode)[0] + reissueRequestedStates(issuerNode, reissuanceRequest, listOf()) + verifyStatesAfterReissuance() + } + @Test fun `DummyStateRequiringAcceptance is re-issued`() { initialiseParties() diff --git a/dummy_workflows/src/test/kotlin/com/r3/corda/lib/reissuance/dummy_flows/RejectReissuanceRequestTest.kt b/dummy_workflows/src/test/kotlin/com/r3/corda/lib/reissuance/dummy_flows/RejectReissuanceRequestTest.kt index 9ae6ec1..8ad363a 100644 --- a/dummy_workflows/src/test/kotlin/com/r3/corda/lib/reissuance/dummy_flows/RejectReissuanceRequestTest.kt +++ b/dummy_workflows/src/test/kotlin/com/r3/corda/lib/reissuance/dummy_flows/RejectReissuanceRequestTest.kt @@ -52,6 +52,24 @@ class RejectReissuanceRequestTest: AbstractFlowTest() { verifyReissuanceRequestRejection() } + @Test + fun `SimpleDummyState re-issuance request is rejected on notary2`() { + initialiseParties() + createSimpleDummyStateOnNotary(aliceParty, notary2Party) + + val simpleDummyState = getStateAndRefs(aliceNode)[0] + createReissuanceRequestAndShareRequiredTransactions( + aliceNode, + listOf(simpleDummyState), + SimpleDummyStateContract.Commands.Create(), + issuerParty + ) + + val reissuanceRequest = getStateAndRefs(issuerNode)[0] + rejectReissuanceRequested(issuerNode, reissuanceRequest) + verifyReissuanceRequestRejection() + } + @Test fun `DummyStateRequiringAcceptance re-issuance request is rejected`() { initialiseParties() diff --git a/dummy_workflows/src/test/kotlin/com/r3/corda/lib/reissuance/dummy_flows/RequestReissuanceTest.kt b/dummy_workflows/src/test/kotlin/com/r3/corda/lib/reissuance/dummy_flows/RequestReissuanceTest.kt index 9c4e0a2..4558d1d 100644 --- a/dummy_workflows/src/test/kotlin/com/r3/corda/lib/reissuance/dummy_flows/RequestReissuanceTest.kt +++ b/dummy_workflows/src/test/kotlin/com/r3/corda/lib/reissuance/dummy_flows/RequestReissuanceTest.kt @@ -58,6 +58,27 @@ class RequestReissuanceTest: AbstractFlowTest() { assertThat(simpleDummyStatesAvailableToIssuer, `is`(statesToBeReissued)) } + @Test + fun `SimpleDummyState re-issuance request is created on notary 2`() { + initialiseParties() + createSimpleDummyStateOnNotary(aliceParty, notary2Party) + + val issuanceCommandData = SimpleDummyStateContract.Commands.Create() + val statesToBeReissued = getStateAndRefs(aliceNode) // there is just 1 state + createReissuanceRequestAndShareRequiredTransactions( + aliceNode, + statesToBeReissued, + issuanceCommandData, + issuerParty + ) + + val reissuanceRequests = getStateAndRefs(issuerNode) + verifyReissuanceRequests(reissuanceRequests, issuanceCommandData, statesToBeReissued) + + val simpleDummyStatesAvailableToIssuer = getStateAndRefs(issuerNode) + assertThat(simpleDummyStatesAvailableToIssuer, `is`(statesToBeReissued)) + } + @Test fun `DummyStateRequiringAcceptance re-issuance request is created`() { initialiseParties() @@ -196,7 +217,7 @@ class RequestReissuanceTest: AbstractFlowTest() { assertThat(simpleDummyStatesAvailableToIssuer, `is`(statesToBeReissued)) } - @Test(expected = TransactionVerificationException::class) + @Test(expected = IllegalArgumentException::class) fun `Request re-issuance of 0 states can't be created`() { initialiseParties() createReissuanceRequestAndShareRequiredTransactions( diff --git a/dummy_workflows/src/test/kotlin/com/r3/corda/lib/reissuance/dummy_flows/UnlockReissuedStatesTest.kt b/dummy_workflows/src/test/kotlin/com/r3/corda/lib/reissuance/dummy_flows/UnlockReissuedStatesTest.kt index 7442517..e3599bb 100644 --- a/dummy_workflows/src/test/kotlin/com/r3/corda/lib/reissuance/dummy_flows/UnlockReissuedStatesTest.kt +++ b/dummy_workflows/src/test/kotlin/com/r3/corda/lib/reissuance/dummy_flows/UnlockReissuedStatesTest.kt @@ -13,7 +13,6 @@ import com.r3.corda.lib.reissuance.dummy_states.DummyStateWithInvalidEqualsMetho import com.r3.corda.lib.reissuance.dummy_states.SimpleDummyState import com.r3.corda.lib.reissuance.states.ReissuanceLock import com.r3.corda.lib.reissuance.utils.convertSignedTransactionToByteArray -import com.r3.corda.lib.reissuance.utils.findSignedTransactionTrandsactionById 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.states.FungibleToken @@ -43,6 +42,22 @@ class UnlockReissuedStatesTest: AbstractFlowTest() { return transactionIds } + private fun createStateAndGenerateBackChainOnNotary( + createState: (Party, Party) -> SecureHash, + updateState: (TestStartedNode, Party) -> SecureHash, + notary : Party + ): List { + val transactionIds = mutableListOf() + transactionIds.add(createState(aliceParty, notary)) + transactionIds.add(updateState(aliceNode, bobParty)) + transactionIds.add(updateState(bobNode, charlieParty)) + transactionIds.add(updateState(charlieNode, aliceParty)) + transactionIds.add(updateState(aliceNode, bobParty)) + transactionIds.add(updateState(bobNode, charlieParty)) + transactionIds.add(updateState(charlieNode, aliceParty)) + return transactionIds + } + private fun createStateAndGenerateBackChainForAccount( createState: (TestStartedNode, AbstractParty) -> SecureHash, updateState: (TestStartedNode, AbstractParty) -> SecureHash, @@ -138,6 +153,36 @@ class UnlockReissuedStatesTest: AbstractFlowTest() { reissueStatesTransactionId, unlockReissuedStatesTransactionId)) } + @Test + fun `Re-issued SimpleDummyState is unencumbered after the original state is deleted on notary2`() { + initialiseParties() + val transactionIds = createStateAndGenerateBackChainOnNotary( + ::createSimpleDummyStateOnNotary, + ::updateSimpleDummyState, + notary2Party + ) + verifyTransactionBackChain(transactionIds) + + val statesToReissue = getStateAndRefs(aliceNode) + val requestReissuanceTransactionId = createReissuanceRequestAndShareRequiredTransactions(aliceNode, + statesToReissue, SimpleDummyStateContract.Commands.Create(), issuerParty) + + val reissuanceRequest = getStateAndRefs(issuerNode)[0] + val reissueStatesTransactionId = reissueRequestedStates(issuerNode, reissuanceRequest, + listOf()) + + val exitTransactionId = deleteSimpleDummyState(aliceNode) + val unlockReissuedStatesTransactionId = unlockReissuedState( + aliceNode, listOf(exitTransactionId), SimpleDummyStateContract.Commands.Update(), + getStateAndRefs(aliceNode, encumbered = true), + getStateAndRefs>(aliceNode, encumbered = true)[0] + ) + + verifyUnlockedStates(statesToReissue) + verifyTransactionBackChain(listOf(requestReissuanceTransactionId, + reissueStatesTransactionId, unlockReissuedStatesTransactionId)) + } + @Test fun `Re-issued DummyStateRequiringAcceptance is unencumbered after the original state is deleted`() { initialiseParties() @@ -509,7 +554,8 @@ class UnlockReissuedStatesTest: AbstractFlowTest() { val updateTransactionId = updateSimpleDummyState(aliceNode, bobParty) val transactionByteArray = convertSignedTransactionToByteArray( - findSignedTransactionTrandsactionById(aliceNode.services, updateTransactionId)!!) + aliceNode.services.validatedTransactions.getTransaction(updateTransactionId)!! + ) unlockReissuedStateUsingModifiedFlow( aliceNode, @@ -719,7 +765,8 @@ class UnlockReissuedStatesTest: AbstractFlowTest() { val exitTransactionId = deleteSimpleDummyState(aliceNode) val exitTransactionByteArray = convertSignedTransactionToByteArray( - findSignedTransactionTrandsactionById(aliceNode.services, exitTransactionId)!!) + aliceNode.services.validatedTransactions.getTransaction(exitTransactionId)!! + ) shareTransaction(aliceNode, bobParty, reissuanceTransactionId) shareTransaction(aliceNode, bobParty, exitTransactionId) diff --git a/gradle.properties b/gradle.properties index a0306e9..94ee55a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,12 +1,12 @@ name=Re-issuance CorDapp reissuance_group=com.r3.corda.lib.reissuance -reissuance_version=1.0-SNAPSHOT +reissuance_version=1.0.2-CBDC-SNAPSHOT kotlin.incremental=false corda_release_group=net.corda corda_core_release_group=net.corda -corda_release_version=4.7 -corda_core_release_version=4.7 +corda_release_version=4.8.5.21-CONCLAVE-SNAPSHOT +corda_core_release_version=4.8.5.21-CONCLAVE-SNAPSHOT corda_gradle_plugins_version=5.0.12 kotlin_version=1.2.71 junit_version=4.12 diff --git a/repositories.gradle b/repositories.gradle index 206b9c6..ab5403e 100644 --- a/repositories.gradle +++ b/repositories.gradle @@ -5,4 +5,11 @@ repositories { maven { url 'https://jitpack.io' } maven { url 'https://software.r3.com/artifactory/corda' } maven { url 'https://repo.gradle.org/gradle/libs-releases' } + maven { + url "https://software.r3.com/artifactory/dc-lib-dev" + credentials { + username = System.getenv('CORDA_ARTIFACTORY_USERNAME') ?: System.getProperty('corda.artifactory.username') + password = System.getenv('CORDA_ARTIFACTORY_PASSWORD') ?: System.getProperty('corda.artifactory.password') + } + } } diff --git a/workflows/src/main/kotlin/com/r3/corda/lib/reissuance/flows/DeleteReissuedStatesAndLock.kt b/workflows/src/main/kotlin/com/r3/corda/lib/reissuance/flows/DeleteReissuedStatesAndLock.kt index f9bf117..445bae0 100644 --- a/workflows/src/main/kotlin/com/r3/corda/lib/reissuance/flows/DeleteReissuedStatesAndLock.kt +++ b/workflows/src/main/kotlin/com/r3/corda/lib/reissuance/flows/DeleteReissuedStatesAndLock.kt @@ -1,7 +1,6 @@ package com.r3.corda.lib.reissuance.flows import co.paralleluniverse.fibers.Suspendable -import com.r3.corda.lib.tokens.workflows.utilities.getPreferredNotary import com.r3.corda.lib.reissuance.contracts.ReissuanceLockContract import com.r3.corda.lib.reissuance.states.ReissuanceLock import net.corda.core.contracts.CommandData @@ -26,7 +25,7 @@ class DeleteReissuedStatesAndLock( ): FlowLogic() where T: ContractState { @Suspendable override fun call(): SecureHash { - val notary = getPreferredNotary(serviceHub) + val notary = reissuedStateAndRefs.first().state.notary val reissuanceLock = reissuanceLockStateAndRef.state.data val requester = reissuanceLock.requester diff --git a/workflows/src/main/kotlin/com/r3/corda/lib/reissuance/flows/GetTransactionBackChain.kt b/workflows/src/main/kotlin/com/r3/corda/lib/reissuance/flows/GetTransactionBackChain.kt index 4774dad..3478f2c 100644 --- a/workflows/src/main/kotlin/com/r3/corda/lib/reissuance/flows/GetTransactionBackChain.kt +++ b/workflows/src/main/kotlin/com/r3/corda/lib/reissuance/flows/GetTransactionBackChain.kt @@ -17,22 +17,25 @@ class GetTransactionBackChain( return getTransactionBackChain(transactionId, visitedTransactions, transactionsToVisitQueue) } - private fun getTransactionBackChain( + private tailrec fun getTransactionBackChain( transactionId: SecureHash, visitedTransactions: MutableSet, transactionsToVisit: MutableSet ): Set { - val signedTransaction = serviceHub.validatedTransactions.getTransaction(transactionId) - ?: throw BackChainException("Cannot find transaction with id $transactionId") + val inputs = serviceHub.validatedTransactions.getTransaction(transactionId)?.inputs ?: run { + serviceHub.validatedTransactions.getEncryptedTransaction(transactionId)?.let { encryptedTx -> + serviceHub.encryptedTransactionService.decryptInputAndRefsForNode(encryptedTx).inputs.map { it.ref } + } + } ?: throw BackChainException("Cannot find transaction with id $transactionId") transactionsToVisit.remove(transactionId) visitedTransactions.add(transactionId) - transactionsToVisit.addAll(signedTransaction.inputs.map { it.txhash }) + transactionsToVisit.addAll(inputs.map { it.txhash }) - if(transactionsToVisit.isEmpty()) - return visitedTransactions - return getTransactionBackChain(transactionsToVisit.elementAt(0), visitedTransactions, + return if(transactionsToVisit.isEmpty()) + visitedTransactions + else getTransactionBackChain(transactionsToVisit.elementAt(0), visitedTransactions, transactionsToVisit) } diff --git a/workflows/src/main/kotlin/com/r3/corda/lib/reissuance/flows/ReissueStates.kt b/workflows/src/main/kotlin/com/r3/corda/lib/reissuance/flows/ReissueStates.kt index b358a67..640eeba 100644 --- a/workflows/src/main/kotlin/com/r3/corda/lib/reissuance/flows/ReissueStates.kt +++ b/workflows/src/main/kotlin/com/r3/corda/lib/reissuance/flows/ReissueStates.kt @@ -1,9 +1,11 @@ package com.r3.corda.lib.reissuance.flows import co.paralleluniverse.fibers.Suspendable -import com.r3.corda.lib.tokens.workflows.utilities.getPreferredNotary import com.r3.corda.lib.reissuance.contracts.ReissuanceLockContract import com.r3.corda.lib.reissuance.contracts.ReissuanceRequestContract +import com.r3.corda.lib.reissuance.schemas.ReissuanceDirection +import com.r3.corda.lib.reissuance.services.ReissuedStatesService +import com.r3.corda.lib.reissuance.states.ReissuableState import com.r3.corda.lib.reissuance.states.ReissuanceLock import com.r3.corda.lib.reissuance.states.ReissuanceRequest import net.corda.core.contracts.ContractState @@ -15,6 +17,9 @@ import net.corda.core.flows.* import net.corda.core.identity.AbstractParty import net.corda.core.internal.requiredContractClassName import net.corda.core.node.services.queryBy +import net.corda.core.node.services.vault.DEFAULT_PAGE_NUM +import net.corda.core.node.services.vault.DEFAULT_PAGE_SIZE +import net.corda.core.node.services.vault.PageSpecification import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder @@ -29,9 +34,11 @@ class ReissueStates( @Suspendable override fun call(): SecureHash { + val reissuedStatesService = serviceHub.cordaService(ReissuedStatesService::class.java) + val reissuanceRequest = reissuanceRequestStateAndRef.state.data - val notary = getPreferredNotary(serviceHub) + val notary = reissuanceRequestStateAndRef.state.notary val requester = reissuanceRequest.requester val issuer = reissuanceRequest.issuer val issuerHost = serviceHub.identityService.partyFromKey(issuer.owningKey)!! @@ -44,13 +51,10 @@ class ReissueStates( criteria=QueryCriteria.VaultQueryCriteria(stateRefs = reissuanceRequest.stateRefsToReissue) ).states as List> - @Suppress("UNCHECKED_CAST") - val locks: List>> = - serviceHub.vaultService.queryBy>().states - as List>> - val reissuedStatesRefs = locks.flatMap { it.state.data.originalStates }.map { it.ref } reissuanceRequest.stateRefsToReissue.forEach { - require(!reissuedStatesRefs.contains(it)) { "State ${it} has been already re-issued" } + val dir = ReissuanceDirection.RECEIVED + require( !reissuedStatesService.hasStateRef(it, dir)) { "State ${it} has been already re-issued" } + reissuedStatesService.storeStateRef(it, dir) } require(statesToReissue.size == reissuanceRequest.stateRefsToReissue.size) { @@ -78,8 +82,13 @@ class ReissueStates( statesToReissue .map { it.state.data } .forEach { + val outputState = if (it is ReissuableState<*>) { + it.createReissuance() + } else { + it + } transactionBuilder.addOutputState( - state = it, + state = outputState, contract = it.requiredContractClassName!!, notary = notary, encumbrance = encumbrance) @@ -150,8 +159,18 @@ abstract class ReissueStatesResponder( reissuanceLock = reissuanceLocks[0] "Status or ReissuanceLock is ACTIVE" using ( reissuanceLock.status == ReissuanceLock.ReissuanceLockStatus.ACTIVE) - "StatesAndRef objects in ReissuanceLock must be the same as re-issued states" using ( - reissuanceLock.originalStates.map { it.state.data } == otherOutputs.map { it.data }) + + if (reissuanceLock.originalStates.first().state.data is ReissuableState<*>) { + reissuanceLock.originalStates.forEachIndexed { index, stateAndRef -> + val state = stateAndRef.state.data as ReissuableState + val reissuedState = otherOutputs[index].data + "StatesAndRef objects in ReissuanceLock must be the same as re-issued states" using ( + state.isEqualForReissuance(reissuedState)) + } + } else { + "StatesAndRef objects in ReissuanceLock must be the same as re-issued states" using ( + reissuanceLock.originalStates.map { it.state.data } == otherOutputs.map { it.data }) + } } } diff --git a/workflows/src/main/kotlin/com/r3/corda/lib/reissuance/flows/RejectReissuanceRequest.kt b/workflows/src/main/kotlin/com/r3/corda/lib/reissuance/flows/RejectReissuanceRequest.kt index ed133fa..840e411 100644 --- a/workflows/src/main/kotlin/com/r3/corda/lib/reissuance/flows/RejectReissuanceRequest.kt +++ b/workflows/src/main/kotlin/com/r3/corda/lib/reissuance/flows/RejectReissuanceRequest.kt @@ -3,7 +3,6 @@ package com.r3.corda.lib.reissuance.flows import co.paralleluniverse.fibers.Suspendable import com.r3.corda.lib.reissuance.contracts.ReissuanceRequestContract import com.r3.corda.lib.reissuance.states.ReissuanceRequest -import com.r3.corda.lib.tokens.workflows.utilities.getPreferredNotary import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateAndRef import net.corda.core.crypto.SecureHash @@ -28,7 +27,7 @@ class RejectReissuanceRequest( val requester = reissuanceRequest.requester val requesterHost = serviceHub.identityService.partyFromKey(requester.owningKey)!! - val notary = getPreferredNotary(serviceHub) + val notary = reissuanceRequestStateAndRef.state.notary val signers = listOf(issuer.owningKey) val transactionBuilder = TransactionBuilder(notary = notary) diff --git a/workflows/src/main/kotlin/com/r3/corda/lib/reissuance/flows/RequestReissuance.kt b/workflows/src/main/kotlin/com/r3/corda/lib/reissuance/flows/RequestReissuance.kt index 0a850e8..c2168b1 100644 --- a/workflows/src/main/kotlin/com/r3/corda/lib/reissuance/flows/RequestReissuance.kt +++ b/workflows/src/main/kotlin/com/r3/corda/lib/reissuance/flows/RequestReissuance.kt @@ -21,32 +21,12 @@ class RequestReissuance( private val stateRefsToReissue: List, private val assetIssuanceCommand: CommandData, private val extraAssetIssuanceSigners: List = listOf(), // issuer is always a signer - private val requester: AbstractParty? = null // requester needs to be provided when using accounts + private val requester: AbstractParty? = null, // requester needs to be provided when using accounts + private val notary : Party? = null ) : FlowLogic() where T: ContractState { @Suspendable override fun call(): SecureHash { - if(requester != null) { - val requesterHost = serviceHub.identityService.partyFromKey(requester.owningKey)!! - require(requesterHost == ourIdentity) { "Requester is not a valid account for the host" } - } - val requesterAbstractParty: AbstractParty = requester ?: ourIdentity - - require(!extraAssetIssuanceSigners.contains(issuer)) { - "Issuer is always a signer and shouldn't be passed in as a part of extraAssetIssuanceSigners" } - val issuanceSigners = listOf(issuer) + extraAssetIssuanceSigners - - val signers = listOf(requesterAbstractParty.owningKey) - - val reissuanceRequest = ReissuanceRequest(issuer, requesterAbstractParty, stateRefsToReissue, - assetIssuanceCommand, issuanceSigners) - - val transactionBuilder = TransactionBuilder(notary = getPreferredNotary(serviceHub)) - transactionBuilder.addOutputState(reissuanceRequest) - transactionBuilder.addCommand(ReissuanceRequestContract.Commands.Create(), signers) - - transactionBuilder.verify(serviceHub) - val signedTransaction = serviceHub.signInitialTransaction(transactionBuilder, signers) val issuerHost: Party = serviceHub.identityService.partyFromKey(issuer.owningKey)!! val sessions = listOfNotNull( @@ -55,11 +35,16 @@ class RequestReissuance( ) return subFlow( - FinalityFlow( - transaction = signedTransaction, - sessions = sessions + RequestReissuanceNonInitiating( + sessions, + issuer, + stateRefsToReissue, + assetIssuanceCommand, + extraAssetIssuanceSigners, + requester, + notary ) - ).id + ) } } @@ -70,10 +55,7 @@ class RequestReissuanceResponder( @Suspendable override fun call() { subFlow( - ReceiveFinalityFlow( - otherSession, - statesToRecord = StatesToRecord.ALL_VISIBLE - ) + RequestReissuanceNonInitiatingResponder(otherSession) ) } } diff --git a/workflows/src/main/kotlin/com/r3/corda/lib/reissuance/flows/RequestReissuanceAndShareRequiredTransactions.kt b/workflows/src/main/kotlin/com/r3/corda/lib/reissuance/flows/RequestReissuanceAndShareRequiredTransactions.kt index b3f0cfb..5d8f8d6 100644 --- a/workflows/src/main/kotlin/com/r3/corda/lib/reissuance/flows/RequestReissuanceAndShareRequiredTransactions.kt +++ b/workflows/src/main/kotlin/com/r3/corda/lib/reissuance/flows/RequestReissuanceAndShareRequiredTransactions.kt @@ -12,6 +12,8 @@ import net.corda.core.identity.AbstractParty import net.corda.core.node.StatesToRecord import net.corda.core.node.services.queryBy import net.corda.core.node.services.vault.QueryCriteria +import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.unwrap @InitiatingFlow @StartableByRPC @@ -25,21 +27,26 @@ class RequestReissuanceAndShareRequiredTransactions( @Suspendable override fun call(): SecureHash { - val requestReissuanceTransactionId = subFlow( - RequestReissuance(issuer, stateRefsToReissue, assetIssuanceCommand, extraAssetIssuanceSigners, requester) - ) - val requesterIdentity = requester ?: ourIdentity + require(stateRefsToReissue.isNotEmpty()) { + "stateRefsToReissue can not be empty" + } val refCriteria = QueryCriteria.VaultQueryCriteria(stateRefs = stateRefsToReissue) - val criteria = if(requester == null) refCriteria else { + val criteria = if (requester == null) refCriteria else { val accountUuid = serviceHub.accountService.accountIdForKey(requester.owningKey) require(accountUuid != null) { "UUID for $requester is not found" } val accountCriteria = QueryCriteria.VaultQueryCriteria().withExternalIds(listOf(accountUuid!!)) refCriteria.and(accountCriteria) } + @Suppress("UNCHECKED_CAST") val statesToReissue: List> = serviceHub.vaultService.queryBy(criteria).states - as List> + as List> + + // there can only be a single notary in a reissuance request + val notary = statesToReissue.map { it.state.notary }.toSet().single() + + val requesterIdentity = requester ?: ourIdentity // all states need to have the same participants val participants = statesToReissue[0].state.data.participants @@ -47,20 +54,37 @@ class RequestReissuanceAndShareRequiredTransactions( val requesterHost = serviceHub.identityService.partyFromKey(requesterIdentity.owningKey)!! val issuerHost = serviceHub.identityService.partyFromKey(issuer.owningKey)!! + val transactionsToSend = mutableListOf() + val sessions = (listOf(issuerHost) - requesterHost).map { initiateFlow(it) } + // if issuer is a participant, they already have access to those transactions - if(!participants.contains(issuer) && requesterHost != issuerHost) { + if (!participants.contains(issuer) && requesterHost != issuerHost) { val transactionHashes = stateRefsToReissue.map { it.txhash } - val transactionsToSend = transactionHashes.map { + transactionsToSend.addAll(transactionHashes.map { serviceHub.validatedTransactions.getTransaction(it) ?: throw FlowException("Can't find transaction with hash $it") - } + }) + } + + sessions.forEach { + it.send(transactionsToSend.size) transactionsToSend.forEach { signedTransaction -> - val sendToSession = initiateFlow(issuerHost) - subFlow(SendTransactionFlow(sendToSession, signedTransaction)) + subFlow(SendTransactionFlow(it, signedTransaction, true)) } } - return requestReissuanceTransactionId + + return subFlow( + RequestReissuanceNonInitiating( + sessions, + issuer, + stateRefsToReissue, + assetIssuanceCommand, + extraAssetIssuanceSigners, + requester, + notary + ) + ) } } @@ -69,9 +93,18 @@ class RequestReissuanceAndShareRequiredTransactions( class ReceiveSignedTransaction(val otherSession: FlowSession) : FlowLogic() { @Suspendable override fun call() { - subFlow(ReceiveTransactionFlow( - otherSideSession = otherSession, - statesToRecord = StatesToRecord.ALL_VISIBLE - )) + + val numTxToReceive = otherSession.receive().unwrap { it } + + if (numTxToReceive > 0) { + (1..numTxToReceive).forEach { _ -> + subFlow(ReceiveTransactionFlow( + otherSideSession = otherSession, + statesToRecord = StatesToRecord.ALL_VISIBLE + )) + } + } + + subFlow(RequestReissuanceNonInitiatingResponder(otherSession)) } } diff --git a/workflows/src/main/kotlin/com/r3/corda/lib/reissuance/flows/RequestReissuanceNonInitiating.kt b/workflows/src/main/kotlin/com/r3/corda/lib/reissuance/flows/RequestReissuanceNonInitiating.kt new file mode 100644 index 0000000..8ccd078 --- /dev/null +++ b/workflows/src/main/kotlin/com/r3/corda/lib/reissuance/flows/RequestReissuanceNonInitiating.kt @@ -0,0 +1,73 @@ +package com.r3.corda.lib.reissuance.flows + +import co.paralleluniverse.fibers.Suspendable +import com.r3.corda.lib.reissuance.contracts.ReissuanceRequestContract +import com.r3.corda.lib.reissuance.states.ReissuanceRequest +import com.r3.corda.lib.tokens.workflows.utilities.getPreferredNotary +import net.corda.core.contracts.CommandData +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.StateRef +import net.corda.core.crypto.SecureHash +import net.corda.core.flows.* +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.Party +import net.corda.core.node.StatesToRecord +import net.corda.core.transactions.TransactionBuilder + +class RequestReissuanceNonInitiating( + private val sessions: List, + private val issuer: AbstractParty, + private val stateRefsToReissue: List, + private val assetIssuanceCommand: CommandData, + private val extraAssetIssuanceSigners: List = listOf(), // issuer is always a signer + private val requester: AbstractParty? = null, // requester needs to be provided when using accounts + private val notary : Party? = null +) : FlowLogic() where T: ContractState { + + @Suspendable + override fun call(): SecureHash { + if(requester != null) { + val requesterHost = serviceHub.identityService.partyFromKey(requester.owningKey)!! + require(requesterHost == ourIdentity) { "Requester is not a valid account for the host" } + } + val requesterAbstractParty: AbstractParty = requester ?: ourIdentity + + require(!extraAssetIssuanceSigners.contains(issuer)) { + "Issuer is always a signer and shouldn't be passed in as a part of extraAssetIssuanceSigners" } + val issuanceSigners = listOf(issuer) + extraAssetIssuanceSigners + + val signers = listOf(requesterAbstractParty.owningKey) + + val reissuanceRequest = ReissuanceRequest(issuer, requesterAbstractParty, stateRefsToReissue, + assetIssuanceCommand, issuanceSigners) + + val notaryToUse = notary ?: getPreferredNotary(serviceHub) + val transactionBuilder = TransactionBuilder(notaryToUse) + transactionBuilder.addOutputState(reissuanceRequest) + transactionBuilder.addCommand(ReissuanceRequestContract.Commands.Create(), signers) + + transactionBuilder.verify(serviceHub) + val signedTransaction = serviceHub.signInitialTransaction(transactionBuilder, signers) + + return subFlow( + FinalityFlow( + transaction = signedTransaction, + sessions = sessions + ) + ).id + } +} + +class RequestReissuanceNonInitiatingResponder( + private val otherSession: FlowSession +) : FlowLogic() { + @Suspendable + override fun call() { + subFlow( + ReceiveFinalityFlow( + otherSession, + statesToRecord = StatesToRecord.ALL_VISIBLE + ) + ) + } +} diff --git a/workflows/src/main/kotlin/com/r3/corda/lib/reissuance/flows/UnlockReissuedStates.kt b/workflows/src/main/kotlin/com/r3/corda/lib/reissuance/flows/UnlockReissuedStates.kt index 24374d6..9a26dc9 100644 --- a/workflows/src/main/kotlin/com/r3/corda/lib/reissuance/flows/UnlockReissuedStates.kt +++ b/workflows/src/main/kotlin/com/r3/corda/lib/reissuance/flows/UnlockReissuedStates.kt @@ -1,11 +1,9 @@ package com.r3.corda.lib.reissuance.flows import co.paralleluniverse.fibers.Suspendable -import com.r3.corda.lib.tokens.workflows.utilities.getPreferredNotary import com.r3.corda.lib.reissuance.contracts.ReissuanceLockContract import com.r3.corda.lib.reissuance.states.ReissuanceLock import com.r3.corda.lib.reissuance.utils.convertSignedTransactionToByteArray -import com.r3.corda.lib.reissuance.utils.findSignedTransactionTrandsactionById import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.flows.* @@ -26,7 +24,7 @@ class UnlockReissuedStates( @Suspendable override fun call(): SecureHash { val assetExitAttachments = assetExitTransactionIds.map { transactionId -> - val signedTransaction = findSignedTransactionTrandsactionById(serviceHub, transactionId) + val signedTransaction = serviceHub.validatedTransactions.getTransaction(transactionId) require(signedTransaction != null) { "Transaction with id $transactionId not found" } val transactionByteArray = convertSignedTransactionToByteArray(signedTransaction!!) serviceHub.attachments.importAttachment(transactionByteArray.inputStream(), ourIdentity.toString(), null) @@ -36,7 +34,7 @@ class UnlockReissuedStates( val requesterHost = serviceHub.identityService.partyFromKey(requester.owningKey)!! require(requesterHost == ourIdentity) { "Requester is not a valid account for the host" } - val notary = getPreferredNotary(serviceHub) + val notary = reissuanceLock.state.notary val lockSigners = listOf(requester.owningKey) require(!extraAssetUnencumberCommandSigners.contains(requester)) { diff --git a/workflows/src/main/kotlin/com/r3/corda/lib/reissuance/schemas/PersistentReissuedState.kt b/workflows/src/main/kotlin/com/r3/corda/lib/reissuance/schemas/PersistentReissuedState.kt new file mode 100644 index 0000000..774f3c7 --- /dev/null +++ b/workflows/src/main/kotlin/com/r3/corda/lib/reissuance/schemas/PersistentReissuedState.kt @@ -0,0 +1,51 @@ +package com.r3.corda.lib.reissuance.schemas + +import net.corda.core.contracts.StateRef +import net.corda.core.schemas.MappedSchema +import java.io.Serializable +import javax.persistence.* + + +object PersistentReissuedStateSchema + +object PersistentReissuedStateSchemaV1 : MappedSchema( + schemaFamily = PersistentReissuedStateSchema.javaClass, + version = 1, + mappedTypes = listOf(PersistentReissuedState::class.java) +) + +enum class ReissuanceDirection { + SENT, + RECEIVED +} + +@Embeddable +data class PersistentReissuedStateId( + @Column(name = "state_ref_hash", nullable = false) + var stateRefHash: String, + + @Column(name = "state_ref_index", nullable = false) + var stateRefIndex: Int, + + @Enumerated(EnumType.STRING) + @Column(name = "direction", nullable = false) + var direction: ReissuanceDirection +) : Serializable + +@Entity(name = "PersistentReissuedState") +@Table(name = "reissued_state") +data class PersistentReissuedState( + + @EmbeddedId + var txhashAndIndex: PersistentReissuedStateId + +) : Serializable { + constructor(stateRef: StateRef, direction: ReissuanceDirection) : + this( + PersistentReissuedStateId( + stateRef.txhash.toHexString(), + stateRef.index, + direction + ) + ) +} \ No newline at end of file diff --git a/workflows/src/main/kotlin/com/r3/corda/lib/reissuance/services/ReissuedStatesService.kt b/workflows/src/main/kotlin/com/r3/corda/lib/reissuance/services/ReissuedStatesService.kt new file mode 100644 index 0000000..84f2106 --- /dev/null +++ b/workflows/src/main/kotlin/com/r3/corda/lib/reissuance/services/ReissuedStatesService.kt @@ -0,0 +1,28 @@ +package com.r3.corda.lib.reissuance.services + +import com.r3.corda.lib.reissuance.schemas.PersistentReissuedState +import com.r3.corda.lib.reissuance.schemas.ReissuanceDirection +import net.corda.core.contracts.StateRef +import net.corda.core.node.AppServiceHub +import net.corda.core.node.services.CordaService +import net.corda.core.serialization.SingletonSerializeAsToken + +@CordaService +class ReissuedStatesService(private val appServiceHub : AppServiceHub) : SingletonSerializeAsToken() { + + fun hasStateRef(stateRef: StateRef, direction: ReissuanceDirection): Boolean { + val result = appServiceHub.withEntityManager { + find(PersistentReissuedState::class.java, PersistentReissuedState(stateRef, direction).txhashAndIndex) + } + + return result != null + } + + fun storeStateRef(stateRef: StateRef, direction: ReissuanceDirection) { + val entity = PersistentReissuedState(stateRef, direction) + + appServiceHub.withEntityManager { + persist(entity) + } + } +} diff --git a/workflows/src/main/resources/migration/persistent-reissued-state-schema-v1.changelog-master.xml b/workflows/src/main/resources/migration/persistent-reissued-state-schema-v1.changelog-master.xml new file mode 100644 index 0000000..387dcc3 --- /dev/null +++ b/workflows/src/main/resources/migration/persistent-reissued-state-schema-v1.changelog-master.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/workflows/src/main/resources/migration/persistent-reissued-state-schema.changelog-init.xml b/workflows/src/main/resources/migration/persistent-reissued-state-schema.changelog-init.xml new file mode 100644 index 0000000..2e46ce1 --- /dev/null +++ b/workflows/src/main/resources/migration/persistent-reissued-state-schema.changelog-init.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + +