diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt index 89b7d2fdd0..a0957211d6 100644 --- a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt +++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt @@ -98,14 +98,13 @@ class NodeMonitorModel { stateMachineUpdates.startWith(currentStateMachines).subscribe(stateMachineUpdatesSubject) // Vault snapshot (force single page load with MAX_PAGE_SIZE) + updates - val (_, vaultUpdates) = proxy.vaultTrackBy(QueryCriteria.VaultQueryCriteria(Vault.StateStatus.ALL), + val (statesSnapshot, vaultUpdates) = proxy.vaultTrackBy(QueryCriteria.VaultQueryCriteria(Vault.StateStatus.ALL), PageSpecification(DEFAULT_PAGE_NUM, MAX_PAGE_SIZE)) - - val vaultSnapshot = proxy.vaultQueryBy(QueryCriteria.VaultQueryCriteria(Vault.StateStatus.UNCONSUMED), - PageSpecification(DEFAULT_PAGE_NUM, MAX_PAGE_SIZE)) - // We have to fetch the snapshot separately since vault query API doesn't allow different criteria for snapshot and updates. - // TODO : This will create a small window of opportunity for inconsistent updates, might need to change the vault API to handle this case. - val initialVaultUpdate = Vault.Update(setOf(), vaultSnapshot.states.toSet()) + val unconsumedStates = statesSnapshot.states.filterIndexed { index, _ -> + statesSnapshot.statesMetadata[index].status == Vault.StateStatus.UNCONSUMED + }.toSet() + val consumedStates = statesSnapshot.states.toSet() - unconsumedStates + val initialVaultUpdate = Vault.Update(consumedStates, unconsumedStates) vaultUpdates.startWith(initialVaultUpdate).subscribe(vaultUpdatesSubject) // Transactions diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/TransactionDataModel.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/TransactionDataModel.kt index 9e4fb5cf03..6c5ff06851 100644 --- a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/TransactionDataModel.kt +++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/TransactionDataModel.kt @@ -11,13 +11,14 @@ package net.corda.client.jfx.model import javafx.beans.value.ObservableValue +import javafx.collections.FXCollections import javafx.collections.ObservableMap import net.corda.client.jfx.utils.* import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef -import net.corda.core.crypto.SecureHash import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.WireTransaction import org.fxmisc.easybind.EasyBind /** @@ -27,7 +28,8 @@ import org.fxmisc.easybind.EasyBind */ data class PartiallyResolvedTransaction( val transaction: SignedTransaction, - val inputs: List>) { + val inputs: List>, + val outputs: List>) { val id = transaction.id sealed class InputResolution { @@ -39,21 +41,49 @@ data class PartiallyResolvedTransaction( } } + sealed class OutputResolution { + abstract val stateRef: StateRef + + data class Unresolved(override val stateRef: StateRef) : OutputResolution() + data class Resolved(val stateAndRef: StateAndRef) : OutputResolution() { + override val stateRef: StateRef get() = stateAndRef.ref + } + } + companion object { fun fromSignedTransaction( transaction: SignedTransaction, - transactions: ObservableMap + stateMap: ObservableMap> ) = PartiallyResolvedTransaction( transaction = transaction, - inputs = transaction.tx.inputs.map { stateRef -> - EasyBind.map(transactions.getObservableValue(stateRef.txhash)) { + inputs = transaction.inputs.map { stateRef -> + EasyBind.map(stateMap.getObservableValue(stateRef)) { if (it == null) { InputResolution.Unresolved(stateRef) } else { - InputResolution.Resolved(it.tx.outRef(stateRef.index)) + InputResolution.Resolved(it) + } + } + }, + outputs = if (transaction.coreTransaction is WireTransaction) { + transaction.tx.outRefsOfType().map { + OutputResolution.Resolved(it).lift() + } + } else { + // Transaction will have the same number of outputs as inputs + val outputCount = transaction.coreTransaction.inputs.size + val stateRefs = (0 until outputCount).map { StateRef(transaction.id, it) } + stateRefs.map { stateRef -> + EasyBind.map(stateMap.getObservableValue(stateRef)) { + if (it == null) { + OutputResolution.Unresolved(stateRef) + } else { + OutputResolution.Resolved(it) + } } } } + ) } } @@ -64,9 +94,13 @@ data class PartiallyResolvedTransaction( class TransactionDataModel { private val transactions by observable(NodeMonitorModel::transactions) private val collectedTransactions = transactions.recordInSequence() - private val transactionMap = transactions.recordAsAssociation(SignedTransaction::id) + private val vaultUpdates by observable(NodeMonitorModel::vaultUpdates) + private val stateMap = vaultUpdates.fold(FXCollections.observableHashMap>()) { map, update -> + val states = update.consumed + update.produced + states.forEach { map[it.ref] = it } + } val partiallyResolvedTransactions = collectedTransactions.map { - PartiallyResolvedTransaction.fromSignedTransaction(it, transactionMap) + PartiallyResolvedTransaction.fromSignedTransaction(it, stateMap) } } diff --git a/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt b/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt index c939f427b4..3e505a2f02 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/SecureHash.kt @@ -96,13 +96,31 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) { /** * A SHA-256 hash value consisting of 32 0x00 bytes. + * This field provides more intuitive access from Java. */ - val zeroHash = SecureHash.SHA256(ByteArray(32, { 0.toByte() })) + @JvmField + val zeroHash: SHA256 = SecureHash.SHA256(ByteArray(32, { 0.toByte() })) + + /** + * A SHA-256 hash value consisting of 32 0x00 bytes. + * This function is provided for API stability. + */ + @Suppress("Unused") + fun getZeroHash(): SHA256 = zeroHash /** * A SHA-256 hash value consisting of 32 0xFF bytes. + * This field provides more intuitive access from Java. */ - val allOnesHash = SecureHash.SHA256(ByteArray(32, { 255.toByte() })) + @JvmField + val allOnesHash: SHA256 = SecureHash.SHA256(ByteArray(32, { 255.toByte() })) + + /** + * A SHA-256 hash value consisting of 32 0xFF bytes. + * This function is provided for API stability. + */ + @Suppress("Unused") + fun getAllOnesHash(): SHA256 = allOnesHash } // In future, maybe SHA3, truncated hashes etc. diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt index cb3cd88d3a..c6fffc0552 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt @@ -40,6 +40,7 @@ import net.corda.client.jfx.utils.* import net.corda.core.contracts.ContractState import net.corda.core.identity.Party import net.corda.core.node.NodeInfo +import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.toBase58String import net.corda.explorer.formatters.PartyNameFormatter import net.corda.explorer.model.CordaView @@ -91,7 +92,11 @@ class Network : CordaView() { .map { it as? PartiallyResolvedTransaction.InputResolution.Resolved } .filterNotNull() .map { it.stateAndRef.state.data }.getParties() - val outputParties = it.transaction.tx.outputStates.observable().getParties() + val outputParties = it.transaction.coreTransaction.let { + if (it is WireTransaction) it.outputStates.observable().getParties() + // For ContractUpgradeWireTransaction and NotaryChangeWireTransaction the output parties are the same as input parties + else inputParties + } val signingParties = it.transaction.sigs.map { it.by.toKnownParty() } // Input parties fire a bullets to all output parties, then to the signing parties and then signing parties to output parties. // !! This is a rough guess of how the message moves in the network. diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/TransactionViewer.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/TransactionViewer.kt index 6723e0a799..cf47c68ea2 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/TransactionViewer.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/TransactionViewer.kt @@ -37,6 +37,8 @@ import net.corda.core.crypto.toStringShort import net.corda.core.identity.AbstractParty import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party +import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.toBase58String import net.corda.sample.businessnetwork.iou.IOUState import net.corda.explorer.AmountDiff @@ -82,7 +84,7 @@ class TransactionViewer : CordaView("Transactions") { val tx: PartiallyResolvedTransaction, val id: SecureHash, val inputs: Inputs, - val outputs: ObservableList>, + val outputs: Outputs, val inputParties: ObservableList>>, val outputParties: ObservableList>>, val commandTypes: List>, @@ -90,6 +92,7 @@ class TransactionViewer : CordaView("Transactions") { ) data class Inputs(val resolved: ObservableList>, val unresolved: ObservableList) + data class Outputs(val resolved: ObservableList>, val unresolved: ObservableList) override fun onDock() { txIdToScroll?.let { @@ -116,38 +119,42 @@ class TransactionViewer : CordaView("Transactions") { */ init { val transactions = transactions.map { - val resolved = it.inputs.sequence() + val resolvedInputs = it.inputs.sequence() .map { it as? PartiallyResolvedTransaction.InputResolution.Resolved } .filterNotNull() .map { it.stateAndRef } - val unresolved = it.inputs.sequence() + val unresolvedInputs = it.inputs.sequence() .map { it as? PartiallyResolvedTransaction.InputResolution.Unresolved } .filterNotNull() .map { it.stateRef } - val outputs = it.transaction.tx.outputs - .mapIndexed { index, transactionState -> - val stateRef = StateRef(it.id, index) - StateAndRef(transactionState, stateRef) - }.observable() + val resolvedOutputs = it.outputs.sequence() + .map { it as? PartiallyResolvedTransaction.OutputResolution.Resolved } + .filterNotNull() + .map { it.stateAndRef } + val unresolvedOutputs = it.inputs.sequence() + .map { it as? PartiallyResolvedTransaction.InputResolution.Unresolved } + .filterNotNull() + .map { it.stateRef } + val commands = if (it.transaction.coreTransaction is WireTransaction) it.transaction.tx.commands else emptyList() Transaction( tx = it, id = it.id, - inputs = Inputs(resolved, unresolved), - outputs = outputs, - inputParties = resolved.getParties(), - outputParties = outputs.getParties(), - commandTypes = it.transaction.tx.commands.map { it.value.javaClass }, + inputs = Inputs(resolvedInputs, unresolvedInputs), + outputs = Outputs(resolvedOutputs, unresolvedOutputs), + inputParties = resolvedInputs.getParties(), + outputParties = resolvedOutputs.getParties(), + commandTypes = commands.map { it.value.javaClass }, totalValueEquiv = ::calculateTotalEquiv.lift(myIdentity, reportingExchange, - resolved.map { it.state.data }.lift(), - it.transaction.tx.outputStates.lift()) + resolvedInputs.map { it.state.data }.lift(), + resolvedOutputs.map { it.state.data }.lift()) ) } val searchField = SearchField(transactions, "Transaction ID" to { tx, s -> "${tx.id}".contains(s, true) }, "Input" to { tx, s -> tx.inputs.resolved.any { it.state.contract.contains(s, true) } }, - "Output" to { tx, s -> tx.outputs.any { it.state.contract.contains(s, true) } }, + "Output" to { tx, s -> tx.outputs.resolved.any { it.state.contract.contains(s, true) } }, "Input Party" to { tx, s -> tx.inputParties.any { it.any { it.value?.name?.organisation?.contains(s, true) == true } } }, "Output Party" to { tx, s -> tx.outputParties.any { it.any { it.value?.name?.organisation?.contains(s, true) == true } } }, "Command Type" to { tx, s -> tx.commandTypes.any { it.simpleName.contains(s, true) } } @@ -174,7 +181,15 @@ class TransactionViewer : CordaView("Transactions") { text += "Unresolved(${it.unresolved.size})" } } - column("Output", Transaction::outputs).cellFormat { text = it.toText() } + column("Output", Transaction::outputs).cellFormat { + text = it.resolved.toText() + if (!it.unresolved.isEmpty()) { + if (!text.isBlank()) { + text += ", " + } + text += "Unresolved(${it.unresolved.size})" + } + } column("Input Party", Transaction::inputParties).setCustomCellFactory { label { text = it.formatJoinPartyNames(formatter = PartyNameFormatter.short) @@ -251,14 +266,14 @@ class TransactionViewer : CordaView("Transactions") { val signatureData = transaction.tx.transaction.sigs.map { it.by } // Bind count to TitlePane inputPane.text = "Input (${transaction.inputs.resolved.count()})" - outputPane.text = "Output (${transaction.outputs.count()})" + outputPane.text = "Output (${transaction.outputs.resolved.count()})" signaturesPane.text = "Signatures (${signatureData.count()})" inputs.cellCache { getCell(it) } outputs.cellCache { getCell(it) } inputs.items = transaction.inputs.resolved - outputs.items = transaction.outputs.observable() + outputs.items = transaction.outputs.resolved signatures.children.addAll(signatureData.map { signature -> val party = signature.toKnownParty()