mirror of
https://github.com/corda/corda.git
synced 2025-06-12 20:28:18 +00:00
CORDA-499: Restructure TransactionGraphSearch to be Dokka-friendly (#1409)
* Remove internal state of TransactionGraphSearch from being publicly visible. * Add Dokka comments for TransactionGraphSearch.Query values. * Move query into TransactionGraphSearch constructor as it should always be set except for test cases. * Move TransactionGraphSearch into trader demo
This commit is contained in:
@ -0,0 +1,74 @@
|
||||
package net.corda.traderdemo
|
||||
|
||||
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.node.services.TransactionStorage
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import java.util.*
|
||||
import java.util.concurrent.Callable
|
||||
|
||||
/**
|
||||
* Given a map of transaction id to [SignedTransaction], performs a breadth first search of the dependency graph from
|
||||
* the starting point down in order to find transactions that match the given query criteria.
|
||||
*
|
||||
* Currently, only one kind of query is supported: find any transaction that contains a command of the given type.
|
||||
*
|
||||
* In future, this should support restricting the search by time, and other types of useful query.
|
||||
*
|
||||
* @property transactions map of transaction id to [SignedTransaction].
|
||||
* @property startPoints transactions to use as starting points for the search.
|
||||
* @property query query to test transactions within the graph for matching.
|
||||
*/
|
||||
class TransactionGraphSearch(private val transactions: TransactionStorage,
|
||||
private val startPoints: List<WireTransaction>,
|
||||
private val query: Query) : Callable<List<WireTransaction>> {
|
||||
/**
|
||||
* Query criteria to match transactions against.
|
||||
*
|
||||
* @property withCommandOfType contract command class to restrict matches to, or null for no filtering by command. Matches the class or
|
||||
* any subclass.
|
||||
* @property followInputsOfType contract output state class to follow the corresponding inputs to. Matches this exact class only.
|
||||
*/
|
||||
data class Query(
|
||||
val withCommandOfType: Class<out CommandData>? = null,
|
||||
val followInputsOfType: Class<out ContractState>? = null
|
||||
) {
|
||||
/**
|
||||
* Test if the given transaction matches this query. Currently only supports checking if the transaction that
|
||||
* contains a command of the given type.
|
||||
*/
|
||||
fun matches(tx: WireTransaction): Boolean {
|
||||
if (withCommandOfType != null) {
|
||||
if (tx.commands.any { it.value.javaClass.isAssignableFrom(withCommandOfType) })
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override fun call(): List<WireTransaction> {
|
||||
val alreadyVisited = HashSet<SecureHash>()
|
||||
val next = ArrayList<WireTransaction>(startPoints)
|
||||
|
||||
val results = ArrayList<WireTransaction>()
|
||||
|
||||
while (next.isNotEmpty()) {
|
||||
val tx = next.removeAt(next.lastIndex)
|
||||
|
||||
if (query.matches(tx))
|
||||
results += tx
|
||||
|
||||
val inputsLeadingToUnvisitedTx: Iterable<StateRef> = tx.inputs.filter { it.txhash !in alreadyVisited }
|
||||
val unvisitedInputTxs: Map<SecureHash, SignedTransaction> = inputsLeadingToUnvisitedTx.map { it.txhash }.toHashSet().map { transactions.getTransaction(it) }.filterNotNull().associateBy { it.id }
|
||||
val unvisitedInputTxsWithInputIndex: Iterable<Pair<SignedTransaction, Int>> = inputsLeadingToUnvisitedTx.filter { it.txhash in unvisitedInputTxs.keys }.map { Pair(unvisitedInputTxs[it.txhash]!!, it.index) }
|
||||
next += (unvisitedInputTxsWithInputIndex.filter { (stx, idx) ->
|
||||
query.followInputsOfType == null || stx.tx.outputs[idx].data.javaClass == query.followInputsOfType
|
||||
}.map { it.first }.filter { stx -> alreadyVisited.add(stx.id) }.map { it.tx })
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
}
|
@ -2,7 +2,6 @@ package net.corda.traderdemo.flow
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.TransactionGraphSearch
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.InitiatedBy
|
||||
import net.corda.core.identity.Party
|
||||
@ -14,6 +13,7 @@ import net.corda.core.utilities.unwrap
|
||||
import net.corda.finance.contracts.CommercialPaper
|
||||
import net.corda.finance.contracts.getCashBalances
|
||||
import net.corda.finance.flows.TwoPartyTradeFlow
|
||||
import net.corda.traderdemo.TransactionGraphSearch
|
||||
import java.util.*
|
||||
|
||||
@InitiatedBy(SellerFlow::class)
|
||||
@ -53,9 +53,12 @@ class BuyerFlow(val otherParty: Party) : FlowLogic<Unit>() {
|
||||
|
||||
private fun logIssuanceAttachment(tradeTX: SignedTransaction) {
|
||||
// Find the original CP issuance.
|
||||
val search = TransactionGraphSearch(serviceHub.validatedTransactions, listOf(tradeTX.tx))
|
||||
search.query = TransactionGraphSearch.Query(withCommandOfType = CommercialPaper.Commands.Issue::class.java,
|
||||
followInputsOfType = CommercialPaper.State::class.java)
|
||||
// TODO: This is potentially very expensive, and requires transaction details we may no longer have once
|
||||
// SGX is enabled. Should be replaced with including the attachment on all transactions involving
|
||||
// the state.
|
||||
val search = TransactionGraphSearch(serviceHub.validatedTransactions, listOf(tradeTX.tx),
|
||||
TransactionGraphSearch.Query(withCommandOfType = CommercialPaper.Commands.Issue::class.java,
|
||||
followInputsOfType = CommercialPaper.State::class.java))
|
||||
val cpIssuance = search.call().single()
|
||||
|
||||
// Buyer will fetch the attachment from the seller automatically when it resolves the transaction.
|
||||
|
@ -0,0 +1,83 @@
|
||||
package net.corda.traderdemo
|
||||
|
||||
import net.corda.core.contracts.CommandData
|
||||
import net.corda.core.crypto.newSecureRandom
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.contracts.DummyState
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.node.MockTransactionStorage
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class TransactionGraphSearchTests : TestDependencyInjectionBase() {
|
||||
class GraphTransactionStorage(val originTx: SignedTransaction, val inputTx: SignedTransaction) : MockTransactionStorage() {
|
||||
init {
|
||||
addTransaction(originTx)
|
||||
addTransaction(inputTx)
|
||||
}
|
||||
}
|
||||
|
||||
fun random31BitValue(): Int = Math.abs(newSecureRandom().nextInt())
|
||||
|
||||
/**
|
||||
* Build a pair of transactions. The first issues a dummy output state, and has a command applied, the second then
|
||||
* references that state.
|
||||
*
|
||||
* @param command the command to add to the origin transaction.
|
||||
* @param signer signer for the two transactions and their commands.
|
||||
*/
|
||||
fun buildTransactions(command: CommandData): GraphTransactionStorage {
|
||||
val megaCorpServices = MockServices(MEGA_CORP_KEY)
|
||||
val notaryServices = MockServices(DUMMY_NOTARY_KEY)
|
||||
|
||||
val originBuilder = TransactionBuilder(DUMMY_NOTARY)
|
||||
.addOutputState(DummyState(random31BitValue()))
|
||||
.addCommand(command, MEGA_CORP_PUBKEY)
|
||||
|
||||
val originPtx = megaCorpServices.signInitialTransaction(originBuilder)
|
||||
val originTx = notaryServices.addSignature(originPtx)
|
||||
|
||||
val inputBuilder = TransactionBuilder(DUMMY_NOTARY)
|
||||
.addInputState(originTx.tx.outRef<DummyState>(0))
|
||||
.addCommand(dummyCommand(MEGA_CORP_PUBKEY))
|
||||
|
||||
val inputPtx = megaCorpServices.signInitialTransaction(inputBuilder)
|
||||
val inputTx = megaCorpServices.addSignature(inputPtx)
|
||||
|
||||
return GraphTransactionStorage(originTx, inputTx)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `return empty from empty`() {
|
||||
val storage = buildTransactions(DummyContract.Commands.Create())
|
||||
val search = TransactionGraphSearch(storage, emptyList(), TransactionGraphSearch.Query())
|
||||
val expected = emptyList<WireTransaction>()
|
||||
val actual = search.call()
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `return empty from no match`() {
|
||||
val storage = buildTransactions(DummyContract.Commands.Create())
|
||||
val search = TransactionGraphSearch(storage, listOf(storage.inputTx.tx), TransactionGraphSearch.Query())
|
||||
val expected = emptyList<WireTransaction>()
|
||||
val actual = search.call()
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `return origin on match`() {
|
||||
val storage = buildTransactions(DummyContract.Commands.Create())
|
||||
val search = TransactionGraphSearch(storage, listOf(storage.inputTx.tx), TransactionGraphSearch.Query(DummyContract.Commands.Create::class.java))
|
||||
val expected = listOf(storage.originTx.tx)
|
||||
val actual = search.call()
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user