diff --git a/Tokens/stockpaydividend/bridging-contracts/src/main/kotlin/com/r3/corda/lib/tokens/bridging/contracts/BridgingContract.kt b/Tokens/stockpaydividend/bridging-contracts/src/main/kotlin/com/r3/corda/lib/tokens/bridging/contracts/BridgingContract.kt index 3577c93b..1f3bd171 100644 --- a/Tokens/stockpaydividend/bridging-contracts/src/main/kotlin/com/r3/corda/lib/tokens/bridging/contracts/BridgingContract.kt +++ b/Tokens/stockpaydividend/bridging-contracts/src/main/kotlin/com/r3/corda/lib/tokens/bridging/contracts/BridgingContract.kt @@ -37,7 +37,7 @@ class BridgingContract : Contract { require(moveCommands.size == 1) { "Bridging must have one move command to lock token" } val lockedSum = tx.outputsOfType() - .filter { it.holder == bridgingCommand.bridgeAuthority } // TODO this is mute point for now, change to != bridgeAuthority, to filter only states owned by CI ... + .filter { it.holder != bridgingCommand.bridgeAuthority } // ... currently can't distinguish between locked and a change, both are for same holder .sumOf { it.amount.toDecimal().toLong() diff --git a/Tokens/stockpaydividend/bridging-flows/src/main/kotlin/com/r3/corda/lib/tokens/bridging/flows/BridgeFungibleTokenFlow.kt b/Tokens/stockpaydividend/bridging-flows/src/main/kotlin/com/r3/corda/lib/tokens/bridging/flows/BridgeFungibleTokenFlow.kt index 9145456c..99ca9d97 100644 --- a/Tokens/stockpaydividend/bridging-flows/src/main/kotlin/com/r3/corda/lib/tokens/bridging/flows/BridgeFungibleTokenFlow.kt +++ b/Tokens/stockpaydividend/bridging-flows/src/main/kotlin/com/r3/corda/lib/tokens/bridging/flows/BridgeFungibleTokenFlow.kt @@ -32,7 +32,7 @@ import net.corda.solana.sdk.instruction.Pubkey class BridgeFungibleTokenFlow( val holder: AbstractParty, val observers: List = emptyList(), - val token: StateAndRef, //TODO should be FungibleToken, TODO change to any TokenType would need amendments to UUID retrieval below + val token: StateAndRef, val bridgeAuthority: Party ) : FlowLogic() { @@ -46,7 +46,7 @@ class BridgeFungibleTokenFlow( val cordaTokenId = (token.state.data.amount.token.tokenType as TokenPointer<*>).pointer.pointer.id - val owners = previousOwnersOf(token).map { serviceHub.identityService.wellKnownPartyFromAnonymous(it) ?: it } + val owners = previousOwnersOf(serviceHub, token).map { serviceHub.identityService.wellKnownPartyFromAnonymous(it) ?: it } val singlePreviousOwner = owners.singleOrNull { it is Party } as Party? require(singlePreviousOwner != null) { "Cannot find previous owner of the token to bridge, or multiple found: $owners" @@ -70,21 +70,11 @@ class BridgeFungibleTokenFlow( additionalCommand = additionalCommand, destination = destination, mint = mint, - mintAuthority = mintAuthority + mintAuthority = mintAuthority, + holder ) ) } - - fun previousOwnersOf(output: StateAndRef): Set { - val txHash = output.ref.txhash - val stx = serviceHub.validatedTransactions.getTransaction(txHash) - ?: error("Producing transaction $txHash not found") - - val inputTokens: List = - stx.toLedgerTransaction(serviceHub).inputsOfType() - - return inputTokens.map { it.holder }.toSet() - } } /** @@ -106,14 +96,14 @@ constructor( val additionalCommand: BridgingContract.BridgingCommand, val destination: Pubkey, val mint: Pubkey, - val mintAuthority: Pubkey + val mintAuthority: Pubkey, + val holder: AbstractParty ) : AbstractMoveTokensFlow() { //TODO move away from this abstract class, it's progress tracker mention only token move @Suspendable override fun addMove(transactionBuilder: TransactionBuilder) { val amount = token.state.data.amount - val holder = ourIdentity //TODO confidential identity val output = FungibleToken(amount, holder) addMoveTokens(transactionBuilder = transactionBuilder, inputs = listOf(token), outputs = listOf(output)) diff --git a/Tokens/stockpaydividend/bridging-flows/src/main/kotlin/com/r3/corda/lib/tokens/bridging/flows/BridgeTokensUtilities.kt b/Tokens/stockpaydividend/bridging-flows/src/main/kotlin/com/r3/corda/lib/tokens/bridging/flows/BridgeTokensUtilities.kt index ecb5f602..84e8d07f 100644 --- a/Tokens/stockpaydividend/bridging-flows/src/main/kotlin/com/r3/corda/lib/tokens/bridging/flows/BridgeTokensUtilities.kt +++ b/Tokens/stockpaydividend/bridging-flows/src/main/kotlin/com/r3/corda/lib/tokens/bridging/flows/BridgeTokensUtilities.kt @@ -3,9 +3,11 @@ package com.r3.corda.lib.tokens.bridging.flows import co.paralleluniverse.fibers.Suspendable import com.r3.corda.lib.tokens.bridging.contracts.BridgingContract 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 net.corda.core.contracts.ContractState import net.corda.core.contracts.StateAndRef +import net.corda.core.identity.AbstractParty import net.corda.core.node.ServiceHub import net.corda.core.transactions.TransactionBuilder import net.corda.solana.sdk.instruction.Pubkey @@ -80,4 +82,16 @@ fun bridgeToken( serviceHub, transactionBuilder, listOf(additionalOutput), additionalCommand, destination, mint, mintAuthority, quantity ) +} + +@Suspendable +fun previousOwnersOf(serviceHub: ServiceHub, output: StateAndRef): Set { + val txHash = output.ref.txhash + val stx = serviceHub.validatedTransactions.getTransaction(txHash) + ?: error("Producing transaction $txHash not found") + + val inputTokens: List = + stx.toLedgerTransaction(serviceHub).inputsOfType() + + return inputTokens.map { it.holder }.toSet() } \ No newline at end of file diff --git a/Tokens/stockpaydividend/bridging-flows/src/main/kotlin/com/r3/corda/lib/tokens/bridging/flows/BridgingAuthorityBootstrapService.kt b/Tokens/stockpaydividend/bridging-flows/src/main/kotlin/com/r3/corda/lib/tokens/bridging/flows/BridgingAuthorityBootstrapService.kt new file mode 100644 index 00000000..d2aea5d8 --- /dev/null +++ b/Tokens/stockpaydividend/bridging-flows/src/main/kotlin/com/r3/corda/lib/tokens/bridging/flows/BridgingAuthorityBootstrapService.kt @@ -0,0 +1,87 @@ +package com.r3.corda.lib.tokens.bridging.flows + +import com.r3.corda.lib.tokens.contracts.states.FungibleToken +import net.corda.core.contracts.StateAndRef +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.PartyAndCertificate +import net.corda.core.node.AppServiceHub +import net.corda.core.node.services.CordaService +import net.corda.core.serialization.SingletonSerializeAsToken +import net.corda.core.utilities.debug +import org.slf4j.LoggerFactory +import java.util.* +import java.util.concurrent.Executors + +@CordaService +class BridgingAuthorityBootstrapService(appServiceHub: AppServiceHub) : SingletonSerializeAsToken() { + private val holdingIdentityPartyAndCertificate: PartyAndCertificate + private val bridgeAuthorityParty = appServiceHub.myInfo.legalIdentities.first() + private val logger = LoggerFactory.getLogger(BridgingAuthorityBootstrapService::class.java) + + private val executor = Executors.newSingleThreadExecutor() + + init { + val cfg = appServiceHub.getAppContext().config + val holdingIdentityLabel = UUID.fromString(cfg.getString("holdingIdentityLabel")) + val holdingIdentityPublicKey = appServiceHub + .identityService + .publicKeysForExternalId(holdingIdentityLabel) + .singleOrNull() + holdingIdentityPartyAndCertificate = if (holdingIdentityPublicKey == null) { + // Generate a new key pair and self-signed certificate for the holding identity + appServiceHub.keyManagementService.freshKeyAndCert( + identity = requireNotNull(appServiceHub.identityService.certificateFromKey(bridgeAuthorityParty.owningKey)) { + "Could not find certificate for key ${bridgeAuthorityParty.owningKey}" + }, + revocationEnabled = false, + externalId = holdingIdentityLabel + ) + } else { + // Reuse the existing key pair and certificate for the holding identity + checkNotNull(appServiceHub.identityService.certificateFromKey(holdingIdentityPublicKey)) { + "Could not find certificate for key $holdingIdentityPublicKey" + } + } + + appServiceHub.registerUnloadHandler { onStop() } + onStartup(appServiceHub) + } + + private fun onStop() { + executor.shutdown() + } + + private fun onStartup(appServiceHub: AppServiceHub) { + //Retrieve states from receiver + val receivedStates = appServiceHub.vaultService.queryBy(FungibleToken::class.java).states + + + callFlow(receivedStates, appServiceHub) + addVaultListener(appServiceHub) + } + + private fun addVaultListener(appServiceHub: AppServiceHub) { + appServiceHub.vaultService.trackBy(FungibleToken::class.java).updates.subscribe { + val producedStockStates = it.produced + callFlow(producedStockStates, appServiceHub) + } + } + + private fun callFlow(fungibleTokens: Collection>, appServiceHub: AppServiceHub) { + fungibleTokens.forEach { token -> + if (bridgeAuthorityParty !in previousOwnersOf(appServiceHub, token)) { + logger.debug { "Starting flow to bridge ${token.state.data.amount} to Solana" } + executor.submit { + appServiceHub.startFlow( + BridgeFungibleTokenFlow( + holdingIdentityPartyAndCertificate.party, + emptyList(), + token, + bridgeAuthorityParty + ) + ) + } + } + } + } +} diff --git a/Tokens/stockpaydividend/bridging-flows/src/main/kotlin/com/r3/corda/lib/tokens/bridging/flows/rpc/BridgeTokens.kt b/Tokens/stockpaydividend/bridging-flows/src/main/kotlin/com/r3/corda/lib/tokens/bridging/flows/rpc/BridgeTokens.kt deleted file mode 100644 index b956034c..00000000 --- a/Tokens/stockpaydividend/bridging-flows/src/main/kotlin/com/r3/corda/lib/tokens/bridging/flows/rpc/BridgeTokens.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.r3.corda.lib.tokens.bridging.flows.rpc - -import co.paralleluniverse.fibers.Suspendable -import com.r3.corda.lib.tokens.bridging.flows.BridgeFungibleTokenFlow -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.InitiatingFlow -import net.corda.core.flows.StartableByRPC -import net.corda.core.identity.Party -import com.r3.corda.lib.tokens.contracts.states.FungibleToken -import net.corda.core.contracts.StateAndRef -import net.corda.core.utilities.ProgressTracker - -@InitiatingFlow -@StartableByRPC -class BridgeToken( - val token: StateAndRef, //TODO change to a list? - val bridgeAuthority: Party -) : FlowLogic() { - - override val progressTracker = ProgressTracker() - - @Suspendable - override fun call(): String { - - //Use built-in flow for move tokens to the recipient - val stx = subFlow( - BridgeFungibleTokenFlow( - ourIdentity, //TODO confidentialIdentity - emptyList(), - token, - bridgeAuthority - ) - ) - - return ("\nBridged quantity " + token.state.data.amount + " " + " stocks to " - + ourIdentity.name.organisation + ".\nTransaction ID: " + stx.id) - } -} - diff --git a/Tokens/stockpaydividend/workflows/src/test/kotlin/net/corda/samples/stockpaydividend/FlowTests.kt b/Tokens/stockpaydividend/workflows/src/test/kotlin/net/corda/samples/stockpaydividend/FlowTests.kt index 3a883d2a..de1ec5b3 100644 --- a/Tokens/stockpaydividend/workflows/src/test/kotlin/net/corda/samples/stockpaydividend/FlowTests.kt +++ b/Tokens/stockpaydividend/workflows/src/test/kotlin/net/corda/samples/stockpaydividend/FlowTests.kt @@ -2,7 +2,6 @@ package net.corda.samples.stockpaydividend import com.lmax.solana4j.api.PublicKey import com.r3.corda.lib.tokens.bridging.states.BridgedAssetLockState -import com.r3.corda.lib.tokens.bridging.flows.rpc.BridgeToken import com.r3.corda.lib.tokens.contracts.states.FungibleToken import com.r3.corda.lib.tokens.contracts.types.TokenPointer import com.r3.corda.lib.tokens.workflows.utilities.tokenBalance @@ -11,7 +10,9 @@ import net.corda.core.contracts.UniqueIdentifier import net.corda.core.crypto.SecureHash import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party -import net.corda.samples.stockpaydividend.flows.* +import net.corda.samples.stockpaydividend.flows.CreateAndIssueStock +import net.corda.samples.stockpaydividend.flows.GetTokenToBridge +import net.corda.samples.stockpaydividend.flows.MoveStock import net.corda.samples.stockpaydividend.states.StockState import net.corda.solana.aggregator.common.RpcParams import net.corda.solana.aggregator.common.Signer @@ -19,26 +20,15 @@ import net.corda.solana.aggregator.common.checkResponse import net.corda.solana.sdk.internal.Token2022 import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.TestIdentity -import net.corda.testing.node.MockNetwork -import net.corda.testing.node.MockNetworkParameters -import net.corda.testing.node.StartedMockNode -import net.corda.testing.node.TestCordapp +import net.corda.testing.node.* import net.corda.testing.solana.SolanaTestValidator -import org.junit.After -import org.junit.AfterClass -import org.junit.Assert -import org.junit.Before -import org.junit.BeforeClass -import org.junit.Test -import java.math.BigDecimal -import java.util.* -import java.util.concurrent.ExecutionException -import net.corda.testing.node.MockNetworkNotarySpec -import net.corda.testing.node.MockNodeParameters import net.corda.testing.solana.randomKeypairFile -import org.junit.ClassRule +import org.junit.* import org.junit.rules.TemporaryFolder +import java.math.BigDecimal import java.nio.file.Path +import java.util.* +import java.util.concurrent.ExecutionException class FlowTests { private var network: MockNetwork? = null @@ -126,7 +116,8 @@ class FlowTests { val baConfig = mapOf( "participants" to mapOf(COMPANY.name.toString() to tokenAccount.base58()), "mints" to mapOf(LINEAR_ID.toString() to tokenMint.base58()), - "mintAuthorities" to mapOf(LINEAR_ID.toString() to mintAuthority.account.base58()) + "mintAuthorities" to mapOf(LINEAR_ID.toString() to mintAuthority.account.base58()), + "holdingIdentityLabel" to UUID.randomUUID().toString() ) network = MockNetwork( MockNetworkParameters( @@ -142,7 +133,8 @@ class FlowTests { notaryConfig = createNotaryConfig() ) ), //TODO start separately notary to provide specific set of cordapps without bridging ones - networkParameters = testNetworkParameters(minimumPlatformVersion = 4) + networkParameters = testNetworkParameters(minimumPlatformVersion = 4), + threadPerNode = true ) ) @@ -200,7 +192,6 @@ class FlowTests { notaryParty!! ) ) - network!!.runNetwork() val stx = future.get() val stxID = stx.substring(stx.lastIndexOf(" ") + 1) val stxIDHash: SecureHash = SecureHash.parse(stxID) @@ -228,12 +219,10 @@ class FlowTests { notaryParty!! ) ) - network!!.runNetwork() future.get() // Move Stock future = company!!.startFlow(MoveStock(STOCK_SYMBOL, BUYING_STOCK, shareholder!!.info.legalIdentities[0])) - network!!.runNetwork() future.get() //Retrieve states from receiver @@ -260,7 +249,6 @@ class FlowTests { @Test @Throws(ExecutionException::class, InterruptedException::class) fun bridgeTest() { - // Issue 1st Stock on Company node var future = company!!.startFlow( CreateAndIssueStock( @@ -273,9 +261,7 @@ class FlowTests { LINEAR_ID ) ) - network!!.runNetwork() future.get() - // Issue 2nd Stock on Bridge Authority node to verify it remains unaffected future = bridgingAuthority!!.startFlow( CreateAndIssueStock( @@ -288,9 +274,7 @@ class FlowTests { LINEAR_ID_2 ) ) - network!!.runNetwork() future.get() - // First stock to be bridged - moving from Company to Bridge Authority var stockStatePointer = getTokensPointer(company!!, STOCK_SYMBOL) val (startCordaQuantity) = company!!.services.vaultService.tokenBalance(stockStatePointer) @@ -305,7 +289,6 @@ class FlowTests { var stock2StatePointer = getTokensPointer(bridgingAuthority!!, STOCK_SYMBOL_2) var (start2CordaQuantity) = bridgingAuthority!!.services.vaultService.tokenBalance(stock2StatePointer) Assert.assertEquals(ISSUING_STOCK_QUANTITY.toLong(), start2CordaQuantity) - // Move Stock future = company!!.startFlow( @@ -315,7 +298,6 @@ class FlowTests { bridgingAuthority!!.info.legalIdentities[0] ) ) - network!!.runNetwork() future.get() // Company has no longer the amount of stocks @@ -329,25 +311,16 @@ class FlowTests { ) Assert.assertEquals(ISSUING_STOCK_QUANTITY.toLong(), startBridgingAuthorityCordaQuantity) - val future2 = bridgingAuthority!!.startFlow( GetTokenToBridge( STOCK_SYMBOL ) ) - network!!.runNetwork() val statesToBridge = future2.get() Assert.assertEquals(1, statesToBridge.size) - future = bridgingAuthority!!.startFlow( - BridgeToken( - statesToBridge.first(), - bridgingAuthority!!.info.legalIdentities[0] //TODO remove this as will be internally moved to own CI - ) - ) - - network!!.runNetwork() - future.get() + // We need to wait for the vault listener to process the newly received token + Thread.sleep(1000) stockStatePointer = getTokensPointer(bridgingAuthority!!, STOCK_SYMBOL) val (finalCordaQuantity) = bridgingAuthority!!.services.vaultService.tokenBalance(stockStatePointer)