1
.gitignore
vendored
@ -18,6 +18,7 @@ tags
|
|||||||
/docs/build/doctrees
|
/docs/build/doctrees
|
||||||
/test-utils/build
|
/test-utils/build
|
||||||
/client/build
|
/client/build
|
||||||
|
/explorer/build
|
||||||
|
|
||||||
# gradle's buildSrc build/
|
# gradle's buildSrc build/
|
||||||
/buildSrc/build/
|
/buildSrc/build/
|
||||||
|
3
.idea/modules.xml
generated
@ -18,6 +18,9 @@
|
|||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/experimental/experimental.iml" filepath="$PROJECT_DIR$/.idea/modules/experimental/experimental.iml" group="experimental" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/experimental/experimental.iml" filepath="$PROJECT_DIR$/.idea/modules/experimental/experimental.iml" group="experimental" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/experimental/experimental_main.iml" filepath="$PROJECT_DIR$/.idea/modules/experimental/experimental_main.iml" group="experimental" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/experimental/experimental_main.iml" filepath="$PROJECT_DIR$/.idea/modules/experimental/experimental_main.iml" group="experimental" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/experimental/experimental_test.iml" filepath="$PROJECT_DIR$/.idea/modules/experimental/experimental_test.iml" group="experimental" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/experimental/experimental_test.iml" filepath="$PROJECT_DIR$/.idea/modules/experimental/experimental_test.iml" group="experimental" />
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/explorer/explorer.iml" filepath="$PROJECT_DIR$/.idea/modules/explorer/explorer.iml" group="explorer" />
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/explorer/explorer_main.iml" filepath="$PROJECT_DIR$/.idea/modules/explorer/explorer_main.iml" group="explorer" />
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/explorer/explorer_test.iml" filepath="$PROJECT_DIR$/.idea/modules/explorer/explorer_test.iml" group="explorer" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated.iml" group="contracts/isolated" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated.iml" group="contracts/isolated" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated_main.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated_main.iml" group="contracts/isolated" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated_main.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated_main.iml" group="contracts/isolated" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated_test.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated_test.iml" group="contracts/isolated" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated_test.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated_test.iml" group="contracts/isolated" />
|
||||||
|
@ -105,9 +105,9 @@ class WalletMonitorClientTests {
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
expect { tx: ServiceToClientEvent.Transaction ->
|
expect { tx: ServiceToClientEvent.Transaction ->
|
||||||
require(tx.transaction.tx.inputs.isEmpty())
|
require(tx.transaction.inputs.isEmpty())
|
||||||
require(tx.transaction.tx.outputs.size == 1)
|
require(tx.transaction.outputs.size == 1)
|
||||||
val signaturePubKeys = tx.transaction.sigs.map { it.by }.toSet()
|
val signaturePubKeys = tx.transaction.mustSign.toSet()
|
||||||
// Only Alice signed
|
// Only Alice signed
|
||||||
require(signaturePubKeys.size == 1)
|
require(signaturePubKeys.size == 1)
|
||||||
require(signaturePubKeys.contains(aliceNode.identity.owningKey))
|
require(signaturePubKeys.contains(aliceNode.identity.owningKey))
|
||||||
@ -137,9 +137,9 @@ class WalletMonitorClientTests {
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
expect { tx: ServiceToClientEvent.Transaction ->
|
expect { tx: ServiceToClientEvent.Transaction ->
|
||||||
require(tx.transaction.tx.inputs.size == 1)
|
require(tx.transaction.inputs.size == 1)
|
||||||
require(tx.transaction.tx.outputs.size == 1)
|
require(tx.transaction.outputs.size == 1)
|
||||||
val signaturePubKeys = tx.transaction.sigs.map { it.by }.toSet()
|
val signaturePubKeys = tx.transaction.mustSign.toSet()
|
||||||
// Alice and Notary signed
|
// Alice and Notary signed
|
||||||
require(signaturePubKeys.size == 2)
|
require(signaturePubKeys.size == 2)
|
||||||
require(signaturePubKeys.contains(aliceNode.identity.owningKey))
|
require(signaturePubKeys.contains(aliceNode.identity.owningKey))
|
||||||
|
@ -21,7 +21,7 @@ import javafx.collections.ObservableListBase
|
|||||||
* The above will create a list that chooses and delegates to the appropriate filtered list based on the type of filter.
|
* The above will create a list that chooses and delegates to the appropriate filtered list based on the type of filter.
|
||||||
*/
|
*/
|
||||||
class ChosenList<E>(
|
class ChosenList<E>(
|
||||||
private val chosenListObservable: ObservableValue<ObservableList<E>>
|
private val chosenListObservable: ObservableValue<out ObservableList<out E>>
|
||||||
): ObservableListBase<E>() {
|
): ObservableListBase<E>() {
|
||||||
|
|
||||||
private var currentList = chosenListObservable.value
|
private var currentList = chosenListObservable.value
|
||||||
@ -48,7 +48,7 @@ class ChosenList<E>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun pick(list: ObservableList<E>) {
|
private fun pick(list: ObservableList<out E>) {
|
||||||
currentList.removeListener(listener)
|
currentList.removeListener(listener)
|
||||||
list.addListener(listener)
|
list.addListener(listener)
|
||||||
beginChange()
|
beginChange()
|
||||||
|
@ -0,0 +1,117 @@
|
|||||||
|
package com.r3corda.client.fxutils
|
||||||
|
|
||||||
|
import javafx.beans.InvalidationListener
|
||||||
|
import javafx.beans.value.ChangeListener
|
||||||
|
import javafx.beans.value.ObservableValue
|
||||||
|
import javafx.collections.ListChangeListener
|
||||||
|
import javafx.collections.ObservableList
|
||||||
|
import javafx.collections.transformation.TransformationList
|
||||||
|
import org.eclipse.jetty.server.Authentication
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [FlattenedList] flattens the passed in list of [ObservableValue]s so that changes in individual updates to the values
|
||||||
|
* are reflected in the exposed list as expected.
|
||||||
|
*/
|
||||||
|
class FlattenedList<A>(val sourceList: ObservableList<out ObservableValue<out A>>) : TransformationList<A, ObservableValue<out A>>(sourceList) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We maintain an ObservableValue->index map. This is needed because we need the ObservableValue's index in order to
|
||||||
|
* propagate a change and if the listener closure captures the index at the time of the call to
|
||||||
|
* [ObservableValue.addListener] it will become incorrect if the indices shift around later.
|
||||||
|
*
|
||||||
|
* Note that because of the bookkeeping required for this map, any remove operation and any add operation that
|
||||||
|
* inserts to the middle of the list will be O(N) as we need to scan the map and shift indices accordingly.
|
||||||
|
*
|
||||||
|
* Note also that we're wrapping each ObservableValue, this is required because we want to support reusing of
|
||||||
|
* ObservableValues and we need each to have a different hash.
|
||||||
|
*/
|
||||||
|
class WrappedObservableValue<A>(
|
||||||
|
val observableValue: ObservableValue<A>
|
||||||
|
)
|
||||||
|
val indexMap = HashMap<WrappedObservableValue<out A>, Pair<Int, ChangeListener<A>>>()
|
||||||
|
init {
|
||||||
|
sourceList.forEachIndexed { index, observableValue ->
|
||||||
|
val wrappedObservableValue = WrappedObservableValue(observableValue)
|
||||||
|
indexMap[wrappedObservableValue] = Pair(index, createListener(wrappedObservableValue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createListener(wrapped: WrappedObservableValue<out A>): ChangeListener<A> {
|
||||||
|
val listener = ChangeListener<A> { _observableValue, oldValue, newValue ->
|
||||||
|
val currentIndex = indexMap[wrapped]!!.first
|
||||||
|
beginChange()
|
||||||
|
nextReplace(currentIndex, currentIndex + 1, listOf(oldValue))
|
||||||
|
endChange()
|
||||||
|
}
|
||||||
|
wrapped.observableValue.addListener(listener)
|
||||||
|
return listener
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun sourceChanged(c: ListChangeListener.Change<out ObservableValue<out A>>) {
|
||||||
|
beginChange()
|
||||||
|
while (c.next()) {
|
||||||
|
if (c.wasPermutated()) {
|
||||||
|
val from = c.from
|
||||||
|
val to = c.to
|
||||||
|
val permutation = IntArray(to, { c.getPermutation(it) })
|
||||||
|
indexMap.replaceAll { _observableValue, pair -> Pair(permutation[pair.first], pair.second) }
|
||||||
|
nextPermutation(from, to, permutation)
|
||||||
|
} else if (c.wasUpdated()) {
|
||||||
|
throw UnsupportedOperationException("FlattenedList doesn't support Update changes")
|
||||||
|
} else {
|
||||||
|
val removed = c.removed
|
||||||
|
if (removed.size != 0) {
|
||||||
|
// TODO this assumes that if wasAdded() == true then we are adding elements to the getFrom() position
|
||||||
|
val removeStart = c.from
|
||||||
|
val removeRange = c.removed.size
|
||||||
|
val removeEnd = c.from + removeRange
|
||||||
|
val iterator = indexMap.iterator()
|
||||||
|
for (entry in iterator) {
|
||||||
|
val (wrapped, pair) = entry
|
||||||
|
val (index, listener) = pair
|
||||||
|
if (index >= removeStart) {
|
||||||
|
if (index < removeEnd) {
|
||||||
|
wrapped.observableValue.removeListener(listener)
|
||||||
|
iterator.remove()
|
||||||
|
} else {
|
||||||
|
// Shift indices
|
||||||
|
entry.setValue(Pair(index - removeRange, listener))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nextRemove(removeStart, removed.map { it.value })
|
||||||
|
}
|
||||||
|
if (c.wasAdded()) {
|
||||||
|
val addStart = c.from
|
||||||
|
val addEnd = c.to
|
||||||
|
val addRange = addEnd - addStart
|
||||||
|
// If it was a push to the end we don't need to shift indices
|
||||||
|
if (addStart != indexMap.size) {
|
||||||
|
val iterator = indexMap.iterator()
|
||||||
|
for (entry in iterator) {
|
||||||
|
val (index, listener) = entry.value
|
||||||
|
if (index >= addStart) {
|
||||||
|
// Shift indices
|
||||||
|
entry.setValue(Pair(index + addRange, listener))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.addedSubList.forEachIndexed { sublistIndex, observableValue ->
|
||||||
|
val wrapped = WrappedObservableValue(observableValue)
|
||||||
|
indexMap[wrapped] = Pair(addStart + sublistIndex, createListener(wrapped))
|
||||||
|
}
|
||||||
|
nextAdd(addStart, addEnd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
endChange()
|
||||||
|
require(sourceList.size == indexMap.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun get(index: Int) = sourceList.get(index).value
|
||||||
|
|
||||||
|
override fun getSourceIndex(index: Int) = index
|
||||||
|
|
||||||
|
override val size: Int get() = sourceList.size
|
||||||
|
}
|
@ -0,0 +1,104 @@
|
|||||||
|
package com.r3corda.client.fxutils
|
||||||
|
|
||||||
|
import javafx.beans.binding.Bindings
|
||||||
|
import javafx.beans.property.ReadOnlyObjectWrapper
|
||||||
|
import javafx.beans.value.ObservableValue
|
||||||
|
import javafx.collections.ObservableList
|
||||||
|
import javafx.collections.transformation.FilteredList
|
||||||
|
import org.fxmisc.easybind.EasyBind
|
||||||
|
import java.util.function.Predicate
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Here follows utility extension functions that help reduce the visual load when developing RX code. Each function should
|
||||||
|
* have a short accompanying example code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* val person: ObservableValue<Person> = (..)
|
||||||
|
* val personName: ObservableValue<String> = person.map { it.name }
|
||||||
|
*/
|
||||||
|
fun <A, B> ObservableValue<out A>.map(function: (A) -> B): ObservableValue<B> = EasyBind.map(this, function)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* val dogs: ObservableList<Dog> = (..)
|
||||||
|
* val dogOwners: ObservableList<Person> = dogs.map { it.owner }
|
||||||
|
*/
|
||||||
|
fun <A, B> ObservableList<out A>.map(function: (A) -> B): ObservableList<B> = EasyBind.map(this, function)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* val aliceHeight: ObservableValue<Long> = (..)
|
||||||
|
* val bobHeight: ObservableValue<Long> = (..)
|
||||||
|
* fun sumHeight(a: Long, b: Long): Long { .. }
|
||||||
|
*
|
||||||
|
* val aliceBobSumHeight = ::sumHeight.lift(aliceHeight, bobHeight)
|
||||||
|
* val aliceHeightPlus2 = ::sumHeight.lift(aliceHeight, 2L.lift())
|
||||||
|
*/
|
||||||
|
fun <A> A.lift(): ObservableValue<A> = ReadOnlyObjectWrapper(this)
|
||||||
|
fun <A, R> ((A) -> R).lift(
|
||||||
|
arg0: ObservableValue<A>
|
||||||
|
): ObservableValue<R> = EasyBind.map(arg0, this)
|
||||||
|
fun <A, B, R> ((A, B) -> R).lift(
|
||||||
|
arg0: ObservableValue<A>,
|
||||||
|
arg1: ObservableValue<B>
|
||||||
|
): ObservableValue<R> = EasyBind.combine(arg0, arg1, this)
|
||||||
|
fun <A, B, C, R> ((A, B, C) -> R).lift(
|
||||||
|
arg0: ObservableValue<A>,
|
||||||
|
arg1: ObservableValue<B>,
|
||||||
|
arg2: ObservableValue<C>
|
||||||
|
): ObservableValue<R> = EasyBind.combine(arg0, arg1, arg2, this)
|
||||||
|
fun <A, B, C, D, R> ((A, B, C, D) -> R).lift(
|
||||||
|
arg0: ObservableValue<A>,
|
||||||
|
arg1: ObservableValue<B>,
|
||||||
|
arg2: ObservableValue<C>,
|
||||||
|
arg3: ObservableValue<D>
|
||||||
|
): ObservableValue<R> = EasyBind.combine(arg0, arg1, arg2, arg3, this)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* data class Person(val height: ObservableValue<Long>)
|
||||||
|
* val person: ObservableValue<Person> = (..)
|
||||||
|
* val personHeight: ObservableValue<Long> = person.bind { it.height }
|
||||||
|
*/
|
||||||
|
fun <A, B> ObservableValue<out A>.bind(function: (A) -> ObservableValue<out B>): ObservableValue<out B> =
|
||||||
|
// We cast here to enforce variance, flatMap should be covariant
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
EasyBind.monadic(this).flatMap(function as (A) -> ObservableValue<B>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* enum class FilterCriterion { HEIGHT, NAME }
|
||||||
|
* val filterCriterion: ObservableValue<FilterCriterion> = (..)
|
||||||
|
* val people: ObservableList<Person> = (..)
|
||||||
|
* fun filterFunction(filterCriterion: FilterCriterion): (Person) -> Boolean { .. }
|
||||||
|
*
|
||||||
|
* val filteredPeople: ObservableList<Person> = people.filter(filterCriterion.map(filterFunction))
|
||||||
|
*/
|
||||||
|
fun <A> ObservableList<out A>.filter(predicate: ObservableValue<out (A) -> Boolean>): ObservableList<out A> {
|
||||||
|
// We cast here to enforce variance, FilteredList should be covariant
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
return FilteredList<A>(this as ObservableList<A>).apply {
|
||||||
|
predicateProperty().bind(predicate.map { predicateFunction ->
|
||||||
|
Predicate<A> { predicateFunction(it) }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* val people: ObservableList<Person> = (..)
|
||||||
|
* val concatenatedNames = people.fold("", { names, person -> names + person.name })
|
||||||
|
* val concatenatedNames2 = people.map(Person::name).fold("", String::plus)
|
||||||
|
*/
|
||||||
|
fun <A, B> ObservableList<out A>.fold(initial: B, folderFunction: (B, A) -> B): ObservableValue<B> {
|
||||||
|
return Bindings.createObjectBinding({
|
||||||
|
var current = initial
|
||||||
|
forEach {
|
||||||
|
current = folderFunction(current, it)
|
||||||
|
}
|
||||||
|
current
|
||||||
|
}, arrayOf(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* data class Person(val height: ObservableValue<Long>)
|
||||||
|
* val people: ObservableList<Person> = (..)
|
||||||
|
* val heights: ObservableList<Long> = people.map(Person::height).flatten()
|
||||||
|
*/
|
||||||
|
fun <A> ObservableList<out ObservableValue<out A>>.flatten(): ObservableList<out A> = FlattenedList(this)
|
@ -88,12 +88,21 @@ class EventGenerator(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val exitCashGenerator =
|
||||||
|
amountIssuedGenerator.map {
|
||||||
|
ClientToServiceCommand.ExitCash(
|
||||||
|
it.withoutIssuer(),
|
||||||
|
it.token.issuer.reference
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val serviceToClientEventGenerator = Generator.frequency<ServiceToClientEvent>(
|
val serviceToClientEventGenerator = Generator.frequency<ServiceToClientEvent>(
|
||||||
1.0 to outputStateGenerator
|
1.0 to outputStateGenerator
|
||||||
)
|
)
|
||||||
|
|
||||||
val clientToServiceCommandGenerator = Generator.frequency(
|
val clientToServiceCommandGenerator = Generator.frequency(
|
||||||
0.33 to issueCashGenerator,
|
0.4 to issueCashGenerator,
|
||||||
0.33 to moveCashGenerator
|
0.5 to moveCashGenerator,
|
||||||
|
0.1 to exitCashGenerator
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,23 @@
|
|||||||
package com.r3corda.client.model
|
package com.r3corda.client.model
|
||||||
|
|
||||||
|
import com.r3corda.client.fxutils.foldToObservableList
|
||||||
import com.r3corda.contracts.asset.Cash
|
import com.r3corda.contracts.asset.Cash
|
||||||
import com.r3corda.core.contracts.ContractState
|
import com.r3corda.core.contracts.ContractState
|
||||||
import com.r3corda.core.contracts.StateAndRef
|
import com.r3corda.core.contracts.StateAndRef
|
||||||
import com.r3corda.core.contracts.StateRef
|
import com.r3corda.core.contracts.StateRef
|
||||||
import com.r3corda.client.fxutils.foldToObservableList
|
|
||||||
import com.r3corda.node.services.monitor.ServiceToClientEvent
|
import com.r3corda.node.services.monitor.ServiceToClientEvent
|
||||||
import com.r3corda.node.services.monitor.StateSnapshotMessage
|
import com.r3corda.node.services.monitor.StateSnapshotMessage
|
||||||
import javafx.collections.ObservableList
|
import javafx.collections.ObservableList
|
||||||
import kotlinx.support.jdk8.collections.removeIf
|
import kotlinx.support.jdk8.collections.removeIf
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
class StatesDiff<out T : ContractState>(
|
sealed class StatesModification<out T : ContractState>{
|
||||||
|
class Diff<out T : ContractState>(
|
||||||
val added: Collection<StateAndRef<T>>,
|
val added: Collection<StateAndRef<T>>,
|
||||||
val removed: Collection<StateRef>
|
val removed: Collection<StateRef>
|
||||||
)
|
) : StatesModification<T>()
|
||||||
|
class Reset<out T : ContractState>(val states: Collection<StateAndRef<T>>) : StatesModification<T>()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This model exposes the list of owned contract states.
|
* This model exposes the list of owned contract states.
|
||||||
@ -24,16 +27,45 @@ class ContractStateModel {
|
|||||||
private val snapshot: Observable<StateSnapshotMessage> by observable(WalletMonitorModel::snapshot)
|
private val snapshot: Observable<StateSnapshotMessage> by observable(WalletMonitorModel::snapshot)
|
||||||
private val outputStates = serviceToClient.ofType(ServiceToClientEvent.OutputState::class.java)
|
private val outputStates = serviceToClient.ofType(ServiceToClientEvent.OutputState::class.java)
|
||||||
|
|
||||||
val contractStatesDiff = outputStates.map { StatesDiff(it.produced, it.consumed) }
|
val contractStatesDiff: Observable<StatesModification.Diff<ContractState>> =
|
||||||
|
outputStates.map { StatesModification.Diff(it.produced, it.consumed) }
|
||||||
// We filter the diff first rather than the complete contract state list.
|
// We filter the diff first rather than the complete contract state list.
|
||||||
// TODO wire up snapshot once it holds StateAndRefs
|
val cashStatesModification: Observable<StatesModification<Cash.State>> = Observable.merge(
|
||||||
val cashStatesDiff = contractStatesDiff.map {
|
arrayOf(
|
||||||
StatesDiff(it.added.filterIsInstance<StateAndRef<Cash.State>>(), it.removed)
|
contractStatesDiff.map {
|
||||||
|
StatesModification.Diff(it.added.filterCashStateAndRefs(), it.removed)
|
||||||
|
},
|
||||||
|
snapshot.map {
|
||||||
|
StatesModification.Reset(it.contractStates.filterCashStateAndRefs())
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
val cashStates: ObservableList<StateAndRef<Cash.State>> =
|
val cashStates: ObservableList<StateAndRef<Cash.State>> =
|
||||||
cashStatesDiff.foldToObservableList(Unit) { statesDiff, _accumulator, observableList ->
|
cashStatesModification.foldToObservableList(Unit) { statesDiff, _accumulator, observableList ->
|
||||||
|
when (statesDiff) {
|
||||||
|
is StatesModification.Diff -> {
|
||||||
observableList.removeIf { it.ref in statesDiff.removed }
|
observableList.removeIf { it.ref in statesDiff.removed }
|
||||||
observableList.addAll(statesDiff.added)
|
observableList.addAll(statesDiff.added)
|
||||||
}
|
}
|
||||||
|
is StatesModification.Reset -> {
|
||||||
|
observableList.setAll(statesDiff.states)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private fun Collection<StateAndRef<ContractState>>.filterCashStateAndRefs(): List<StateAndRef<Cash.State>> {
|
||||||
|
return this.map { stateAndRef ->
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
if (stateAndRef.state.data is Cash.State) {
|
||||||
|
// Kotlin doesn't unify here for some reason
|
||||||
|
stateAndRef as StateAndRef<Cash.State>
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}.filterNotNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
package com.r3corda.client.model
|
package com.r3corda.client.model
|
||||||
|
|
||||||
import com.r3corda.client.fxutils.foldToObservableList
|
import com.r3corda.client.fxutils.foldToObservableList
|
||||||
import com.r3corda.core.transactions.SignedTransaction
|
import com.r3corda.core.crypto.SecureHash
|
||||||
|
import com.r3corda.core.transactions.LedgerTransaction
|
||||||
import com.r3corda.node.services.monitor.ServiceToClientEvent
|
import com.r3corda.node.services.monitor.ServiceToClientEvent
|
||||||
import com.r3corda.node.services.monitor.TransactionBuildResult
|
import com.r3corda.node.services.monitor.TransactionBuildResult
|
||||||
import com.r3corda.node.utilities.AddOrRemove
|
import com.r3corda.node.utilities.AddOrRemove
|
||||||
import javafx.beans.property.SimpleObjectProperty
|
import javafx.beans.property.SimpleObjectProperty
|
||||||
import javafx.beans.value.ObservableValue
|
import javafx.beans.value.ObservableValue
|
||||||
|
import javafx.collections.FXCollections
|
||||||
import javafx.collections.ObservableList
|
import javafx.collections.ObservableList
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
@ -15,11 +18,12 @@ import java.util.UUID
|
|||||||
interface GatheredTransactionData {
|
interface GatheredTransactionData {
|
||||||
val fiberId: ObservableValue<Long?>
|
val fiberId: ObservableValue<Long?>
|
||||||
val uuid: ObservableValue<UUID?>
|
val uuid: ObservableValue<UUID?>
|
||||||
val protocolName: ObservableValue<String?>
|
|
||||||
val protocolStatus: ObservableValue<ProtocolStatus?>
|
val protocolStatus: ObservableValue<ProtocolStatus?>
|
||||||
val transaction: ObservableValue<SignedTransaction?>
|
val stateMachineStatus: ObservableValue<StateMachineStatus?>
|
||||||
|
val transaction: ObservableValue<LedgerTransaction?>
|
||||||
val status: ObservableValue<TransactionCreateStatus?>
|
val status: ObservableValue<TransactionCreateStatus?>
|
||||||
val lastUpdate: ObservableValue<Instant>
|
val lastUpdate: ObservableValue<Instant>
|
||||||
|
val allEvents: ObservableList<out ServiceToClientEvent>
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class TransactionCreateStatus(val message: String?) {
|
sealed class TransactionCreateStatus(val message: String?) {
|
||||||
@ -28,21 +32,24 @@ sealed class TransactionCreateStatus(val message: String?) {
|
|||||||
override fun toString(): String = message ?: javaClass.simpleName
|
override fun toString(): String = message ?: javaClass.simpleName
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class ProtocolStatus(val status: String?) {
|
data class ProtocolStatus(
|
||||||
object Added: ProtocolStatus(null)
|
val status: String
|
||||||
object Removed: ProtocolStatus(null)
|
)
|
||||||
class InProgress(status: String): ProtocolStatus(status)
|
sealed class StateMachineStatus(val stateMachineName: String) {
|
||||||
override fun toString(): String = status ?: javaClass.simpleName
|
class Added(stateMachineName: String): StateMachineStatus(stateMachineName)
|
||||||
|
class Removed(stateMachineName: String): StateMachineStatus(stateMachineName)
|
||||||
|
override fun toString(): String = "${javaClass.simpleName}($stateMachineName)"
|
||||||
}
|
}
|
||||||
|
|
||||||
data class GatheredTransactionDataWritable(
|
data class GatheredTransactionDataWritable(
|
||||||
override val fiberId: SimpleObjectProperty<Long?> = SimpleObjectProperty(null),
|
override val fiberId: SimpleObjectProperty<Long?> = SimpleObjectProperty(null),
|
||||||
override val uuid: SimpleObjectProperty<UUID?> = SimpleObjectProperty(null),
|
override val uuid: SimpleObjectProperty<UUID?> = SimpleObjectProperty(null),
|
||||||
override val protocolName: SimpleObjectProperty<String?> = SimpleObjectProperty(null),
|
override val stateMachineStatus: SimpleObjectProperty<StateMachineStatus?> = SimpleObjectProperty(null),
|
||||||
override val protocolStatus: SimpleObjectProperty<ProtocolStatus?> = SimpleObjectProperty(null),
|
override val protocolStatus: SimpleObjectProperty<ProtocolStatus?> = SimpleObjectProperty(null),
|
||||||
override val transaction: SimpleObjectProperty<SignedTransaction?> = SimpleObjectProperty(null),
|
override val transaction: SimpleObjectProperty<LedgerTransaction?> = SimpleObjectProperty(null),
|
||||||
override val status: SimpleObjectProperty<TransactionCreateStatus?> = SimpleObjectProperty(null),
|
override val status: SimpleObjectProperty<TransactionCreateStatus?> = SimpleObjectProperty(null),
|
||||||
override val lastUpdate: SimpleObjectProperty<Instant>
|
override val lastUpdate: SimpleObjectProperty<Instant>,
|
||||||
|
override val allEvents: ObservableList<ServiceToClientEvent> = FXCollections.observableArrayList()
|
||||||
) : GatheredTransactionData
|
) : GatheredTransactionData
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -54,7 +61,7 @@ class GatheredTransactionDataModel {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Aggregation of updates to transactions. We use the observable list as the only container and do linear search for
|
* Aggregation of updates to transactions. We use the observable list as the only container and do linear search for
|
||||||
* matching transactions because we have two keys(fiber ID and UUID) and this way it's easier to avoid syncing issues.
|
* matching transactions because we have three keys(fiber ID, UUID, tx id) and this way it's easier to avoid syncing issues.
|
||||||
*
|
*
|
||||||
* The Fiber ID is used to identify events that relate to the same transaction server-side, whereas the UUID is
|
* The Fiber ID is used to identify events that relate to the same transaction server-side, whereas the UUID is
|
||||||
* generated on the UI and is used to identify events with the UI action that triggered them. Currently a UUID is
|
* generated on the UI and is used to identify events with the UI action that triggered them. Currently a UUID is
|
||||||
@ -64,46 +71,49 @@ class GatheredTransactionDataModel {
|
|||||||
* (Note that a transaction may be mapped by one or both)
|
* (Note that a transaction may be mapped by one or both)
|
||||||
* TODO: Expose a writable stream to combine [serviceToClient] with to allow recording of transactions made locally(UUID)
|
* TODO: Expose a writable stream to combine [serviceToClient] with to allow recording of transactions made locally(UUID)
|
||||||
*/
|
*/
|
||||||
val gatheredGatheredTransactionDataList: ObservableList<out GatheredTransactionData> =
|
val gatheredTransactionDataList: ObservableList<out GatheredTransactionData> =
|
||||||
serviceToClient.foldToObservableList<ServiceToClientEvent, GatheredTransactionDataWritable, Unit>(
|
serviceToClient.foldToObservableList<ServiceToClientEvent, GatheredTransactionDataWritable, Unit>(
|
||||||
initialAccumulator = Unit,
|
initialAccumulator = Unit,
|
||||||
folderFun = { serviceToClientEvent, _unit, transactionStates ->
|
folderFun = { serviceToClientEvent, _unit, transactionStates ->
|
||||||
return@foldToObservableList when (serviceToClientEvent) {
|
return@foldToObservableList when (serviceToClientEvent) {
|
||||||
is ServiceToClientEvent.Transaction -> {
|
is ServiceToClientEvent.Transaction -> {
|
||||||
// TODO handle this once we have some id to associate the tx with
|
newTransactionIdTransactionStateOrModify(transactionStates, serviceToClientEvent,
|
||||||
|
transaction = serviceToClientEvent.transaction,
|
||||||
|
tweak = {}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
is ServiceToClientEvent.OutputState -> {}
|
is ServiceToClientEvent.OutputState -> {}
|
||||||
is ServiceToClientEvent.StateMachine -> {
|
is ServiceToClientEvent.StateMachine -> {
|
||||||
newFiberIdTransactionStateOrModify(transactionStates,
|
newFiberIdTransactionStateOrModify(transactionStates, serviceToClientEvent,
|
||||||
fiberId = serviceToClientEvent.fiberId,
|
fiberId = serviceToClientEvent.fiberId,
|
||||||
lastUpdate = serviceToClientEvent.time,
|
|
||||||
tweak = {
|
tweak = {
|
||||||
protocolName.set(serviceToClientEvent.label)
|
stateMachineStatus.set(when (serviceToClientEvent.addOrRemove) {
|
||||||
protocolStatus.set(when (serviceToClientEvent.addOrRemove) {
|
AddOrRemove.ADD -> StateMachineStatus.Added(serviceToClientEvent.label)
|
||||||
AddOrRemove.ADD -> ProtocolStatus.Added
|
AddOrRemove.REMOVE -> StateMachineStatus.Removed(serviceToClientEvent.label)
|
||||||
AddOrRemove.REMOVE -> ProtocolStatus.Removed
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is ServiceToClientEvent.Progress -> {
|
is ServiceToClientEvent.Progress -> {
|
||||||
newFiberIdTransactionStateOrModify(transactionStates,
|
newFiberIdTransactionStateOrModify(transactionStates, serviceToClientEvent,
|
||||||
fiberId = serviceToClientEvent.fiberId,
|
fiberId = serviceToClientEvent.fiberId,
|
||||||
lastUpdate = serviceToClientEvent.time,
|
|
||||||
tweak = {
|
tweak = {
|
||||||
protocolStatus.set(ProtocolStatus.InProgress(serviceToClientEvent.message))
|
protocolStatus.set(ProtocolStatus(serviceToClientEvent.message))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is ServiceToClientEvent.TransactionBuild -> {
|
is ServiceToClientEvent.TransactionBuild -> {
|
||||||
val state = serviceToClientEvent.state
|
val state = serviceToClientEvent.state
|
||||||
newUuidTransactionStateOrModify(transactionStates,
|
newUuidTransactionStateOrModify(transactionStates, serviceToClientEvent,
|
||||||
uuid = serviceToClientEvent.id,
|
uuid = serviceToClientEvent.id,
|
||||||
fiberId = when (state) {
|
fiberId = when (state) {
|
||||||
is TransactionBuildResult.ProtocolStarted -> state.fiberId
|
is TransactionBuildResult.ProtocolStarted -> state.fiberId
|
||||||
is TransactionBuildResult.Failed -> null
|
is TransactionBuildResult.Failed -> null
|
||||||
},
|
},
|
||||||
lastUpdate = serviceToClientEvent.time,
|
transactionId = when (state) {
|
||||||
|
is TransactionBuildResult.ProtocolStarted -> state.transaction?.id
|
||||||
|
is TransactionBuildResult.Failed -> null
|
||||||
|
},
|
||||||
tweak = {
|
tweak = {
|
||||||
return@newUuidTransactionStateOrModify when (state) {
|
return@newUuidTransactionStateOrModify when (state) {
|
||||||
is TransactionBuildResult.ProtocolStarted -> {
|
is TransactionBuildResult.ProtocolStarted -> {
|
||||||
@ -122,50 +132,84 @@ class GatheredTransactionDataModel {
|
|||||||
)
|
)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private fun newFiberIdTransactionStateOrModify(
|
|
||||||
|
private fun newTransactionIdTransactionStateOrModify(
|
||||||
transactionStates: ObservableList<GatheredTransactionDataWritable>,
|
transactionStates: ObservableList<GatheredTransactionDataWritable>,
|
||||||
fiberId: Long,
|
event: ServiceToClientEvent,
|
||||||
lastUpdate: Instant,
|
transaction: LedgerTransaction,
|
||||||
tweak: GatheredTransactionDataWritable.() -> Unit
|
tweak: GatheredTransactionDataWritable.() -> Unit
|
||||||
) {
|
) {
|
||||||
val index = transactionStates.indexOfFirst { it.fiberId.value == fiberId }
|
val index = transactionStates.indexOfFirst { transaction.id == it.transaction.value?.id }
|
||||||
if (index < 0) {
|
val state = if (index < 0) {
|
||||||
val newState = GatheredTransactionDataWritable(
|
val newState = GatheredTransactionDataWritable(
|
||||||
fiberId = SimpleObjectProperty(fiberId),
|
transaction = SimpleObjectProperty(transaction),
|
||||||
lastUpdate = SimpleObjectProperty(lastUpdate)
|
lastUpdate = SimpleObjectProperty(event.time)
|
||||||
)
|
)
|
||||||
tweak(newState)
|
tweak(newState)
|
||||||
transactionStates.add(newState)
|
transactionStates.add(newState)
|
||||||
|
newState
|
||||||
} else {
|
} else {
|
||||||
val existingState = transactionStates[index]
|
val existingState = transactionStates[index]
|
||||||
existingState.lastUpdate.set(lastUpdate)
|
existingState.lastUpdate.set(event.time)
|
||||||
tweak(existingState)
|
tweak(existingState)
|
||||||
|
existingState
|
||||||
}
|
}
|
||||||
|
state.allEvents.add(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun newFiberIdTransactionStateOrModify(
|
||||||
|
transactionStates: ObservableList<GatheredTransactionDataWritable>,
|
||||||
|
event: ServiceToClientEvent,
|
||||||
|
fiberId: Long,
|
||||||
|
tweak: GatheredTransactionDataWritable.() -> Unit
|
||||||
|
) {
|
||||||
|
val index = transactionStates.indexOfFirst { it.fiberId.value == fiberId }
|
||||||
|
val state = if (index < 0) {
|
||||||
|
val newState = GatheredTransactionDataWritable(
|
||||||
|
fiberId = SimpleObjectProperty(fiberId),
|
||||||
|
lastUpdate = SimpleObjectProperty(event.time)
|
||||||
|
)
|
||||||
|
tweak(newState)
|
||||||
|
transactionStates.add(newState)
|
||||||
|
newState
|
||||||
|
} else {
|
||||||
|
val existingState = transactionStates[index]
|
||||||
|
existingState.lastUpdate.set(event.time)
|
||||||
|
tweak(existingState)
|
||||||
|
existingState
|
||||||
|
}
|
||||||
|
state.allEvents.add(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun newUuidTransactionStateOrModify(
|
private fun newUuidTransactionStateOrModify(
|
||||||
transactionStates: ObservableList<GatheredTransactionDataWritable>,
|
transactionStates: ObservableList<GatheredTransactionDataWritable>,
|
||||||
|
event: ServiceToClientEvent,
|
||||||
uuid: UUID,
|
uuid: UUID,
|
||||||
fiberId: Long?,
|
fiberId: Long?,
|
||||||
lastUpdate: Instant,
|
transactionId: SecureHash?,
|
||||||
tweak: GatheredTransactionDataWritable.() -> Unit
|
tweak: GatheredTransactionDataWritable.() -> Unit
|
||||||
) {
|
) {
|
||||||
val index = transactionStates.indexOfFirst {
|
val index = transactionStates.indexOfFirst {
|
||||||
it.uuid.value == uuid || (fiberId != null && it.fiberId.value == fiberId)
|
it.uuid.value == uuid ||
|
||||||
|
(fiberId != null && it.fiberId.value == fiberId) ||
|
||||||
|
(transactionId != null && it.transaction.value?.id == transactionId)
|
||||||
}
|
}
|
||||||
if (index < 0) {
|
val state = if (index < 0) {
|
||||||
val newState = GatheredTransactionDataWritable(
|
val newState = GatheredTransactionDataWritable(
|
||||||
uuid = SimpleObjectProperty(uuid),
|
uuid = SimpleObjectProperty(uuid),
|
||||||
fiberId = SimpleObjectProperty(fiberId),
|
fiberId = SimpleObjectProperty(fiberId),
|
||||||
lastUpdate = SimpleObjectProperty(lastUpdate)
|
lastUpdate = SimpleObjectProperty(event.time)
|
||||||
)
|
)
|
||||||
tweak(newState)
|
tweak(newState)
|
||||||
transactionStates.add(newState)
|
transactionStates.add(newState)
|
||||||
|
newState
|
||||||
} else {
|
} else {
|
||||||
val existingState = transactionStates[index]
|
val existingState = transactionStates[index]
|
||||||
existingState.lastUpdate.set(lastUpdate)
|
existingState.lastUpdate.set(event.time)
|
||||||
tweak(existingState)
|
tweak(existingState)
|
||||||
|
existingState
|
||||||
}
|
}
|
||||||
|
state.allEvents.add(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,28 +8,31 @@ import kotlin.test.fail
|
|||||||
class AggregatedListTest {
|
class AggregatedListTest {
|
||||||
|
|
||||||
var sourceList = FXCollections.observableArrayList<Int>()
|
var sourceList = FXCollections.observableArrayList<Int>()
|
||||||
|
var aggregatedList = AggregatedList(sourceList, { it % 3 }) { mod3, group -> Pair(mod3, group) }
|
||||||
|
var replayedList = ReplayedList(aggregatedList)
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
sourceList = FXCollections.observableArrayList<Int>()
|
sourceList = FXCollections.observableArrayList<Int>()
|
||||||
|
aggregatedList = AggregatedList(sourceList, { it % 3 }) { mod3, group -> Pair(mod3, group) }
|
||||||
|
replayedList = ReplayedList(aggregatedList)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun addWorks() {
|
fun addWorks() {
|
||||||
val aggregatedList = AggregatedList(sourceList, { it % 3 }) { mod3, group -> Pair(mod3, group) }
|
require(replayedList.size == 0) { "Aggregation is empty is source list is" }
|
||||||
require(aggregatedList.size == 0) { "Aggregation is empty is source list is" }
|
|
||||||
|
|
||||||
sourceList.add(9)
|
sourceList.add(9)
|
||||||
require(aggregatedList.size == 1) { "Aggregation list has one element if one was added to source list" }
|
require(replayedList.size == 1) { "Aggregation list has one element if one was added to source list" }
|
||||||
require(aggregatedList[0]!!.first == 0)
|
require(replayedList[0]!!.first == 0)
|
||||||
|
|
||||||
sourceList.add(8)
|
sourceList.add(8)
|
||||||
require(aggregatedList.size == 2) { "Aggregation list has two elements if two were added to source list with different keys" }
|
require(replayedList.size == 2) { "Aggregation list has two elements if two were added to source list with different keys" }
|
||||||
|
|
||||||
sourceList.add(6)
|
sourceList.add(6)
|
||||||
require(aggregatedList.size == 2) { "Aggregation list's size doesn't change if element with existing key is added" }
|
require(replayedList.size == 2) { "Aggregation list's size doesn't change if element with existing key is added" }
|
||||||
|
|
||||||
aggregatedList.forEach {
|
replayedList.forEach {
|
||||||
when (it.first) {
|
when (it.first) {
|
||||||
0 -> require(it.second.toSet() == setOf(6, 9))
|
0 -> require(it.second.toSet() == setOf(6, 9))
|
||||||
2 -> require(it.second.size == 1)
|
2 -> require(it.second.size == 1)
|
||||||
@ -40,11 +43,10 @@ class AggregatedListTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun removeWorks() {
|
fun removeWorks() {
|
||||||
val aggregatedList = AggregatedList(sourceList, { it % 3 }) { mod3, group -> Pair(mod3, group) }
|
|
||||||
sourceList.addAll(0, 1, 2, 3, 4)
|
sourceList.addAll(0, 1, 2, 3, 4)
|
||||||
|
|
||||||
require(aggregatedList.size == 3)
|
require(replayedList.size == 3)
|
||||||
aggregatedList.forEach {
|
replayedList.forEach {
|
||||||
when (it.first) {
|
when (it.first) {
|
||||||
0 -> require(it.second.toSet() == setOf(0, 3))
|
0 -> require(it.second.toSet() == setOf(0, 3))
|
||||||
1 -> require(it.second.toSet() == setOf(1, 4))
|
1 -> require(it.second.toSet() == setOf(1, 4))
|
||||||
@ -54,8 +56,8 @@ class AggregatedListTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sourceList.remove(4)
|
sourceList.remove(4)
|
||||||
require(aggregatedList.size == 3)
|
require(replayedList.size == 3)
|
||||||
aggregatedList.forEach {
|
replayedList.forEach {
|
||||||
when (it.first) {
|
when (it.first) {
|
||||||
0 -> require(it.second.toSet() == setOf(0, 3))
|
0 -> require(it.second.toSet() == setOf(0, 3))
|
||||||
1 -> require(it.second.toSet() == setOf(1))
|
1 -> require(it.second.toSet() == setOf(1))
|
||||||
@ -65,8 +67,8 @@ class AggregatedListTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sourceList.remove(2, 4)
|
sourceList.remove(2, 4)
|
||||||
require(aggregatedList.size == 2)
|
require(replayedList.size == 2)
|
||||||
aggregatedList.forEach {
|
replayedList.forEach {
|
||||||
when (it.first) {
|
when (it.first) {
|
||||||
0 -> require(it.second.toSet() == setOf(0))
|
0 -> require(it.second.toSet() == setOf(0))
|
||||||
1 -> require(it.second.toSet() == setOf(1))
|
1 -> require(it.second.toSet() == setOf(1))
|
||||||
@ -75,7 +77,7 @@ class AggregatedListTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sourceList.removeAll(0, 1)
|
sourceList.removeAll(0, 1)
|
||||||
require(aggregatedList.size == 0)
|
require(replayedList.size == 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,113 @@
|
|||||||
|
package com.r3corda.client.fxutils
|
||||||
|
|
||||||
|
import javafx.beans.property.SimpleObjectProperty
|
||||||
|
import javafx.collections.FXCollections
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class FlattenedListTest {
|
||||||
|
|
||||||
|
var sourceList = FXCollections.observableArrayList(SimpleObjectProperty(1234))
|
||||||
|
var flattenedList = FlattenedList(sourceList)
|
||||||
|
var replayedList = ReplayedList(flattenedList)
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
sourceList = FXCollections.observableArrayList(SimpleObjectProperty(1234))
|
||||||
|
flattenedList = FlattenedList(sourceList)
|
||||||
|
replayedList = ReplayedList(flattenedList)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun addWorks() {
|
||||||
|
require(replayedList.size == 1)
|
||||||
|
require(replayedList[0] == 1234)
|
||||||
|
|
||||||
|
sourceList.add(SimpleObjectProperty(12))
|
||||||
|
require(replayedList.size == 2)
|
||||||
|
require(replayedList[0] == 1234)
|
||||||
|
require(replayedList[1] == 12)
|
||||||
|
|
||||||
|
sourceList.add(SimpleObjectProperty(34))
|
||||||
|
require(replayedList.size == 3)
|
||||||
|
require(replayedList[0] == 1234)
|
||||||
|
require(replayedList[1] == 12)
|
||||||
|
require(replayedList[2] == 34)
|
||||||
|
|
||||||
|
sourceList.add(0, SimpleObjectProperty(56))
|
||||||
|
require(replayedList.size == 4)
|
||||||
|
require(replayedList[0] == 56)
|
||||||
|
require(replayedList[1] == 1234)
|
||||||
|
require(replayedList[2] == 12)
|
||||||
|
require(replayedList[3] == 34)
|
||||||
|
|
||||||
|
sourceList.addAll(2, listOf(SimpleObjectProperty(78), SimpleObjectProperty(910)))
|
||||||
|
require(replayedList.size == 6)
|
||||||
|
require(replayedList[0] == 56)
|
||||||
|
require(replayedList[1] == 1234)
|
||||||
|
require(replayedList[2] == 78)
|
||||||
|
require(replayedList[3] == 910)
|
||||||
|
require(replayedList[4] == 12)
|
||||||
|
require(replayedList[5] == 34)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun removeWorks() {
|
||||||
|
val firstRemoved = sourceList.removeAt(0)
|
||||||
|
require(firstRemoved.get() == 1234)
|
||||||
|
require(replayedList.size == 0)
|
||||||
|
firstRemoved.set(123)
|
||||||
|
|
||||||
|
sourceList.add(SimpleObjectProperty(12))
|
||||||
|
sourceList.add(SimpleObjectProperty(34))
|
||||||
|
sourceList.add(SimpleObjectProperty(56))
|
||||||
|
require(replayedList.size == 3)
|
||||||
|
val secondRemoved = sourceList.removeAt(1)
|
||||||
|
require(secondRemoved.get() == 34)
|
||||||
|
require(replayedList.size == 2)
|
||||||
|
require(replayedList[0] == 12)
|
||||||
|
require(replayedList[1] == 56)
|
||||||
|
secondRemoved.set(123)
|
||||||
|
|
||||||
|
sourceList.clear()
|
||||||
|
require(replayedList.size == 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun updatingObservableWorks() {
|
||||||
|
require(replayedList[0] == 1234)
|
||||||
|
sourceList[0].set(4321)
|
||||||
|
require(replayedList[0] == 4321)
|
||||||
|
|
||||||
|
sourceList.add(0, SimpleObjectProperty(12))
|
||||||
|
sourceList[1].set(8765)
|
||||||
|
require(replayedList[0] == 12)
|
||||||
|
require(replayedList[1] == 8765)
|
||||||
|
|
||||||
|
sourceList[0].set(34)
|
||||||
|
require(replayedList[0] == 34)
|
||||||
|
require(replayedList[1] == 8765)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun reusingObservableWorks() {
|
||||||
|
val observable = SimpleObjectProperty(12)
|
||||||
|
sourceList.add(observable)
|
||||||
|
sourceList.add(observable)
|
||||||
|
require(replayedList.size == 3)
|
||||||
|
require(replayedList[0] == 1234)
|
||||||
|
require(replayedList[1] == 12)
|
||||||
|
require(replayedList[2] == 12)
|
||||||
|
|
||||||
|
observable.set(34)
|
||||||
|
require(replayedList.size == 3)
|
||||||
|
require(replayedList[0] == 1234)
|
||||||
|
require(replayedList[1] == 34)
|
||||||
|
require(replayedList[2] == 34)
|
||||||
|
|
||||||
|
sourceList.removeAt(1)
|
||||||
|
require(replayedList.size == 2)
|
||||||
|
require(replayedList[0] == 1234)
|
||||||
|
require(replayedList[1] == 34)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
package com.r3corda.client.fxutils
|
||||||
|
|
||||||
|
import javafx.collections.ListChangeListener
|
||||||
|
import javafx.collections.ObservableList
|
||||||
|
import javafx.collections.transformation.TransformationList
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This list type just replays changes propagated from the underlying source list. Used for testing changes.
|
||||||
|
*/
|
||||||
|
class ReplayedList<A>(sourceList: ObservableList<A>) : TransformationList<A, A>(sourceList) {
|
||||||
|
|
||||||
|
val replayedList = ArrayList<A>(sourceList)
|
||||||
|
|
||||||
|
override val size: Int get() = replayedList.size
|
||||||
|
|
||||||
|
override fun sourceChanged(c: ListChangeListener.Change<out A>) {
|
||||||
|
|
||||||
|
beginChange()
|
||||||
|
while (c.next()) {
|
||||||
|
if (c.wasPermutated()) {
|
||||||
|
val from = c.from
|
||||||
|
val to = c.to
|
||||||
|
val permutation = IntArray(to, { c.getPermutation(it) })
|
||||||
|
val permutedSubList = ArrayList<A>(to - from)
|
||||||
|
for (i in 0 .. (to - from - 1)) {
|
||||||
|
permutedSubList.add(replayedList[permutation[from + i]])
|
||||||
|
}
|
||||||
|
permutedSubList.forEachIndexed { i, element ->
|
||||||
|
replayedList[from + i] = element
|
||||||
|
}
|
||||||
|
nextPermutation(from, to, permutation)
|
||||||
|
} else if (c.wasUpdated()) {
|
||||||
|
for (i in c.from .. c.to - 1) {
|
||||||
|
replayedList[i] = c.list[i]
|
||||||
|
nextUpdate(i)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (c.wasRemoved()) {
|
||||||
|
// TODO this assumes that if wasAdded() == true then we are adding elements to the getFrom() position
|
||||||
|
val removePosition = c.from
|
||||||
|
for (i in 0 .. c.removedSize - 1) {
|
||||||
|
replayedList.removeAt(removePosition)
|
||||||
|
}
|
||||||
|
nextRemove(c.from, c.removed)
|
||||||
|
}
|
||||||
|
if (c.wasAdded()) {
|
||||||
|
val addStart = c.from
|
||||||
|
val addEnd = c.to
|
||||||
|
for (i in addStart .. addEnd - 1) {
|
||||||
|
replayedList.add(i, c.list[i])
|
||||||
|
}
|
||||||
|
nextAdd(addStart, addEnd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
endChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSourceIndex(index: Int) = index
|
||||||
|
|
||||||
|
override fun get(index: Int) = replayedList[index]
|
||||||
|
}
|
@ -0,0 +1,86 @@
|
|||||||
|
package com.r3corda.client.fxutils
|
||||||
|
|
||||||
|
import javafx.collections.FXCollections
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class ReplayedListTest {
|
||||||
|
|
||||||
|
var sourceList = FXCollections.observableArrayList(1234)
|
||||||
|
var replayedList = ReplayedList(sourceList)
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
sourceList = FXCollections.observableArrayList(1234)
|
||||||
|
replayedList = ReplayedList(sourceList)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun addWorks() {
|
||||||
|
require(replayedList.size == 1)
|
||||||
|
require(replayedList[0] == 1234)
|
||||||
|
|
||||||
|
sourceList.add(12)
|
||||||
|
require(replayedList.size == 2)
|
||||||
|
require(replayedList[0] == 1234)
|
||||||
|
require(replayedList[1] == 12)
|
||||||
|
|
||||||
|
sourceList.add(34)
|
||||||
|
require(replayedList.size == 3)
|
||||||
|
require(replayedList[0] == 1234)
|
||||||
|
require(replayedList[1] == 12)
|
||||||
|
require(replayedList[2] == 34)
|
||||||
|
|
||||||
|
sourceList.add(0, 56)
|
||||||
|
require(replayedList.size == 4)
|
||||||
|
require(replayedList[0] == 56)
|
||||||
|
require(replayedList[1] == 1234)
|
||||||
|
require(replayedList[2] == 12)
|
||||||
|
require(replayedList[3] == 34)
|
||||||
|
|
||||||
|
sourceList.addAll(2, listOf(78, 910))
|
||||||
|
require(replayedList.size == 6)
|
||||||
|
require(replayedList[0] == 56)
|
||||||
|
require(replayedList[1] == 1234)
|
||||||
|
require(replayedList[2] == 78)
|
||||||
|
require(replayedList[3] == 910)
|
||||||
|
require(replayedList[4] == 12)
|
||||||
|
require(replayedList[5] == 34)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun removeWorks() {
|
||||||
|
val firstRemoved = sourceList.removeAt(0)
|
||||||
|
require(firstRemoved == 1234)
|
||||||
|
require(replayedList.size == 0)
|
||||||
|
|
||||||
|
sourceList.add(12)
|
||||||
|
sourceList.add(34)
|
||||||
|
sourceList.add(56)
|
||||||
|
require(replayedList.size == 3)
|
||||||
|
val secondRemoved = sourceList.removeAt(1)
|
||||||
|
require(secondRemoved == 34)
|
||||||
|
require(replayedList.size == 2)
|
||||||
|
require(replayedList[0] == 12)
|
||||||
|
require(replayedList[1] == 56)
|
||||||
|
|
||||||
|
sourceList.clear()
|
||||||
|
require(replayedList.size == 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun updateWorks() {
|
||||||
|
require(replayedList[0] == 1234)
|
||||||
|
sourceList[0] = 4321
|
||||||
|
require(replayedList[0] == 4321)
|
||||||
|
|
||||||
|
sourceList.add(0, 12)
|
||||||
|
sourceList[1] = 8765
|
||||||
|
require(replayedList[0] == 12)
|
||||||
|
require(replayedList[1] == 8765)
|
||||||
|
|
||||||
|
sourceList[0] = 34
|
||||||
|
require(replayedList[0] == 34)
|
||||||
|
require(replayedList[1] == 8765)
|
||||||
|
}
|
||||||
|
}
|
77
explorer/build.gradle
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
group 'com.r3corda'
|
||||||
|
version '1.0-SNAPSHOT'
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
ext.kotlin_version = '1.0.3'
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
maven {
|
||||||
|
url 'https://dl.bintray.com/kotlin/exposed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'java'
|
||||||
|
apply plugin: 'kotlin'
|
||||||
|
apply plugin: 'application'
|
||||||
|
|
||||||
|
sourceCompatibility = 1.8
|
||||||
|
|
||||||
|
applicationDefaultJvmArgs = ["-javaagent:${rootProject.configurations.quasar.singleFile}"]
|
||||||
|
mainClassName = 'com.r3corda.explorer.Main'
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
main {
|
||||||
|
resources {
|
||||||
|
srcDir "../config/dev"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
test {
|
||||||
|
resources {
|
||||||
|
srcDir "../config/test"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
jcenter()
|
||||||
|
mavenCentral()
|
||||||
|
mavenLocal()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
|
testCompile group: 'junit', name: 'junit', version: '4.11'
|
||||||
|
|
||||||
|
// TornadoFX: A lightweight Kotlin framework for working with JavaFX UI's.
|
||||||
|
compile 'no.tornado:tornadofx:1.5.1'
|
||||||
|
|
||||||
|
// Corda Core: Data structures and basic types needed to work with Corda.
|
||||||
|
compile project(':core')
|
||||||
|
compile project(':client')
|
||||||
|
compile project(':node')
|
||||||
|
compile project(':contracts')
|
||||||
|
|
||||||
|
// FontAwesomeFX: The "FontAwesome" icon library.
|
||||||
|
compile 'de.jensd:fontawesomefx-fontawesome:4.6.1-2'
|
||||||
|
|
||||||
|
// ReactFX: Functional reactive UI programming.
|
||||||
|
compile 'org.reactfx:reactfx:2.0-M5'
|
||||||
|
compile 'org.fxmisc.easybind:easybind:1.0.3'
|
||||||
|
|
||||||
|
// JFXtras: useful widgets including a calendar control.
|
||||||
|
compile 'org.jfxtras:jfxtras-agenda:8.0-r5'
|
||||||
|
compile 'org.jfxtras:jfxtras-font-roboto:8.0-r5'
|
||||||
|
|
||||||
|
// Humanize: formatting
|
||||||
|
compile 'com.github.mfornos:humanize-icu:1.2.2'
|
||||||
|
}
|
26
explorer/src/main/kotlin/com/r3corda/explorer/AmountDiff.kt
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package com.r3corda.explorer
|
||||||
|
|
||||||
|
import com.r3corda.core.contracts.Amount
|
||||||
|
|
||||||
|
enum class Positivity {
|
||||||
|
Positive,
|
||||||
|
Negative
|
||||||
|
}
|
||||||
|
|
||||||
|
val Positivity.sign: String get() = when (this) {
|
||||||
|
Positivity.Positive -> ""
|
||||||
|
Positivity.Negative -> "-"
|
||||||
|
}
|
||||||
|
|
||||||
|
data class AmountDiff<T>(
|
||||||
|
val positivity: Positivity,
|
||||||
|
val amount: Amount<T>
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun <T> fromLong(quantity: Long, token: T) =
|
||||||
|
AmountDiff(
|
||||||
|
positivity = if (quantity < 0) Positivity.Negative else Positivity.Positive,
|
||||||
|
amount = Amount(Math.abs(quantity), token)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
82
explorer/src/main/kotlin/com/r3corda/explorer/Main.kt
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package com.r3corda.explorer
|
||||||
|
|
||||||
|
import com.r3corda.client.WalletMonitorClient
|
||||||
|
import com.r3corda.client.mock.EventGenerator
|
||||||
|
import com.r3corda.client.mock.Generator
|
||||||
|
import com.r3corda.client.mock.oneOf
|
||||||
|
import com.r3corda.client.model.Models
|
||||||
|
import com.r3corda.client.model.WalletMonitorModel
|
||||||
|
import com.r3corda.client.model.observer
|
||||||
|
import com.r3corda.core.contracts.ClientToServiceCommand
|
||||||
|
import com.r3corda.explorer.model.IdentityModel
|
||||||
|
import com.r3corda.node.driver.PortAllocation
|
||||||
|
import com.r3corda.node.driver.driver
|
||||||
|
import com.r3corda.node.driver.startClient
|
||||||
|
import com.r3corda.node.services.monitor.ServiceToClientEvent
|
||||||
|
import com.r3corda.node.services.transactions.SimpleNotaryService
|
||||||
|
import javafx.stage.Stage
|
||||||
|
import rx.Observer
|
||||||
|
import rx.subjects.PublishSubject
|
||||||
|
import tornadofx.App
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class Main : App() {
|
||||||
|
override val primaryView = MainWindow::class
|
||||||
|
val aliceOutStream: Observer<ClientToServiceCommand> by observer(WalletMonitorModel::clientToService)
|
||||||
|
|
||||||
|
override fun start(stage: Stage) {
|
||||||
|
|
||||||
|
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
|
||||||
|
throwable.printStackTrace()
|
||||||
|
System.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
super.start(stage)
|
||||||
|
|
||||||
|
// start the driver on another thread
|
||||||
|
// TODO Change this to connecting to an actual node (specified on cli/in a config) once we're happy with the code
|
||||||
|
Thread({
|
||||||
|
|
||||||
|
val portAllocation = PortAllocation.Incremental(20000)
|
||||||
|
driver(portAllocation = portAllocation) {
|
||||||
|
|
||||||
|
val aliceNodeFuture = startNode("Alice")
|
||||||
|
val bobNodeFuture = startNode("Bob")
|
||||||
|
val notaryNodeFuture = startNode("Notary", advertisedServices = setOf(SimpleNotaryService.Type))
|
||||||
|
|
||||||
|
val aliceNode = aliceNodeFuture.get()
|
||||||
|
val bobNode = bobNodeFuture.get()
|
||||||
|
val notaryNode = notaryNodeFuture.get()
|
||||||
|
|
||||||
|
val aliceClient = startClient(aliceNode).get()
|
||||||
|
|
||||||
|
Models.get<IdentityModel>(Main::class).myIdentity.set(aliceNode.identity)
|
||||||
|
Models.get<WalletMonitorModel>(Main::class).register(aliceClient, aliceNode)
|
||||||
|
|
||||||
|
val bobInStream = PublishSubject.create<ServiceToClientEvent>()
|
||||||
|
val bobOutStream = PublishSubject.create<ClientToServiceCommand>()
|
||||||
|
|
||||||
|
val bobClient = startClient(bobNode).get()
|
||||||
|
val bobMonitorClient = WalletMonitorClient(bobClient, bobNode, bobOutStream, bobInStream, PublishSubject.create())
|
||||||
|
assert(bobMonitorClient.register().get())
|
||||||
|
|
||||||
|
for (i in 0 .. 10000) {
|
||||||
|
Thread.sleep(500)
|
||||||
|
|
||||||
|
val eventGenerator = EventGenerator(
|
||||||
|
parties = listOf(aliceNode.identity, bobNode.identity),
|
||||||
|
notary = notaryNode.identity
|
||||||
|
)
|
||||||
|
|
||||||
|
eventGenerator.clientToServiceCommandGenerator.combine(Generator.oneOf(listOf(aliceOutStream, bobOutStream))) {
|
||||||
|
command, stream -> stream.onNext(command)
|
||||||
|
}.generate(Random())
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForAllNodesToFinish()
|
||||||
|
}
|
||||||
|
|
||||||
|
}).start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
26
explorer/src/main/kotlin/com/r3corda/explorer/MainWindow.kt
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package com.r3corda.explorer
|
||||||
|
|
||||||
|
import com.r3corda.explorer.views.TopLevel
|
||||||
|
import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory
|
||||||
|
import jfxtras.resources.JFXtrasFontRoboto
|
||||||
|
import tornadofx.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The root view embeds the [Shell] and provides support for the status bar, and modal dialogs.
|
||||||
|
*/
|
||||||
|
class MainWindow : View() {
|
||||||
|
private val toplevel: TopLevel by inject()
|
||||||
|
override val root = toplevel.root
|
||||||
|
|
||||||
|
init {
|
||||||
|
// Do this first before creating the notification bar, so it can autosize itself properly.
|
||||||
|
loadFontsAndStyles()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadFontsAndStyles() {
|
||||||
|
JFXtrasFontRoboto.loadAll()
|
||||||
|
importStylesheet("/com/r3corda/explorer/css/wallet.css")
|
||||||
|
FontAwesomeIconFactory.get() // Force initialisation.
|
||||||
|
root.styleClass += "root"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
package com.r3corda.explorer.formatters
|
||||||
|
|
||||||
|
import com.r3corda.core.contracts.Amount
|
||||||
|
import java.util.Currency
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A note on formatting: Currently we don't have any fancy locale/use-case-specific formatting of amounts. This is a
|
||||||
|
* non-trivial problem that requires substantial work.
|
||||||
|
* Libraries to evaluate: IBM ICU currency library, github.com/mfornos/humanize, JSR 354 ref. implementation
|
||||||
|
*/
|
||||||
|
|
||||||
|
object AmountFormatter {
|
||||||
|
// TODO replace this once we settled on how we do formatting
|
||||||
|
val boring = object : Formatter<Amount<Currency>> {
|
||||||
|
override fun format(value: Amount<Currency>) = "${value.quantity} ${value.token}"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
package com.r3corda.explorer.formatters
|
||||||
|
|
||||||
|
|
||||||
|
interface Formatter<in T> {
|
||||||
|
fun format(value: T): String
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package com.r3corda.explorer.formatters
|
||||||
|
|
||||||
|
object NumberFormatter {
|
||||||
|
// TODO replace this once we settled on how we do formatting
|
||||||
|
val boring = object : Formatter<Any> {
|
||||||
|
override fun format(value: Any) = value.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
val boringLong: Formatter<Long> = boring
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package com.r3corda.explorer.model
|
||||||
|
|
||||||
|
import com.r3corda.core.crypto.Party
|
||||||
|
import javafx.beans.property.SimpleObjectProperty
|
||||||
|
|
||||||
|
|
||||||
|
class IdentityModel {
|
||||||
|
val myIdentity = SimpleObjectProperty<Party>()
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
package com.r3corda.explorer.model
|
||||||
|
|
||||||
|
import com.r3corda.core.contracts.Amount
|
||||||
|
import com.r3corda.client.fxutils.AmountBindings
|
||||||
|
import com.r3corda.client.model.ExchangeRate
|
||||||
|
import com.r3corda.client.model.ExchangeRateModel
|
||||||
|
import com.r3corda.client.model.observableValue
|
||||||
|
import javafx.beans.value.ObservableValue
|
||||||
|
import org.fxmisc.easybind.EasyBind
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class ReportingCurrencyModel {
|
||||||
|
private val exchangeRate: ObservableValue<ExchangeRate> by observableValue(ExchangeRateModel::exchangeRate)
|
||||||
|
val reportingCurrency: ObservableValue<Currency> by observableValue(SettingsModel::reportingCurrency)
|
||||||
|
/**
|
||||||
|
* This stream provides a stream of exchange() functions that updates when either the reporting currency or the
|
||||||
|
* exchange rates change
|
||||||
|
*/
|
||||||
|
val reportingExchange: ObservableValue<Pair<Currency, (Amount<Currency>) -> Amount<Currency>>> =
|
||||||
|
EasyBind.map(AmountBindings.exchange(reportingCurrency, exchangeRate)) { Pair(it.first) { amount: Amount<Currency> ->
|
||||||
|
Amount(it.second(amount), it.first)
|
||||||
|
}}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
package com.r3corda.explorer.model
|
||||||
|
|
||||||
|
import com.r3corda.core.contracts.USD
|
||||||
|
import javafx.beans.property.SimpleObjectProperty
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class SettingsModel {
|
||||||
|
|
||||||
|
val reportingCurrency: SimpleObjectProperty<Currency> = SimpleObjectProperty(USD)
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
package com.r3corda.explorer.model
|
||||||
|
|
||||||
|
import javafx.beans.property.SimpleObjectProperty
|
||||||
|
|
||||||
|
enum class SelectedView {
|
||||||
|
Home,
|
||||||
|
Cash,
|
||||||
|
Transaction
|
||||||
|
}
|
||||||
|
|
||||||
|
class TopLevelModel {
|
||||||
|
val selectedView = SimpleObjectProperty<SelectedView>(SelectedView.Home)
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
package com.r3corda.explorer.ui
|
||||||
|
|
||||||
|
import com.r3corda.explorer.formatters.Formatter
|
||||||
|
import javafx.scene.Node
|
||||||
|
import javafx.scene.control.ListCell
|
||||||
|
import javafx.scene.control.ListView
|
||||||
|
import javafx.util.Callback
|
||||||
|
|
||||||
|
fun <T> Formatter<T>.toListCellFactory() = Callback<ListView<T?>, ListCell<T?>> {
|
||||||
|
object : ListCell<T?>() {
|
||||||
|
override fun updateItem(value: T?, empty: Boolean) {
|
||||||
|
super.updateItem(value, empty)
|
||||||
|
text = if (value == null || empty) {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
format(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> ListView<T>.setCustomCellFactory(toNode: (T) -> Node) {
|
||||||
|
setCellFactory {
|
||||||
|
object : ListCell<T>() {
|
||||||
|
init {
|
||||||
|
text = null
|
||||||
|
}
|
||||||
|
override fun updateItem(value: T?, empty: Boolean) {
|
||||||
|
super.updateItem(value, empty)
|
||||||
|
graphic = if (value != null && !empty) {
|
||||||
|
toNode(value)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
package com.r3corda.explorer.ui
|
||||||
|
|
||||||
|
sealed class SingleRowSelection<out A> {
|
||||||
|
class None<out A> : SingleRowSelection<A>()
|
||||||
|
class Selected<out A>(val node: A) : SingleRowSelection<A>()
|
||||||
|
}
|
@ -0,0 +1,76 @@
|
|||||||
|
package com.r3corda.explorer.ui
|
||||||
|
|
||||||
|
import com.r3corda.explorer.formatters.Formatter
|
||||||
|
import javafx.beans.binding.Bindings
|
||||||
|
import javafx.beans.value.ObservableValue
|
||||||
|
import javafx.scene.Node
|
||||||
|
import javafx.scene.control.ListCell
|
||||||
|
import javafx.scene.control.TableCell
|
||||||
|
import javafx.scene.control.TableColumn
|
||||||
|
import javafx.scene.control.TableView
|
||||||
|
import javafx.util.Callback
|
||||||
|
import org.fxmisc.easybind.EasyBind
|
||||||
|
|
||||||
|
fun <S> TableView<S>.setColumnPrefWidthPolicy(
|
||||||
|
getColumnWidth: (tableWidthWithoutPaddingAndBorder: Number, column: TableColumn<S, *>) -> Number
|
||||||
|
) {
|
||||||
|
val tableWidthWithoutPaddingAndBorder = Bindings.createDoubleBinding({
|
||||||
|
val padding = padding
|
||||||
|
val borderInsets = border?.insets
|
||||||
|
width -
|
||||||
|
(if (padding != null) padding.left + padding.right else 0.0) -
|
||||||
|
(if (borderInsets != null) borderInsets.left + borderInsets.right else 0.0)
|
||||||
|
}, arrayOf(columns, widthProperty(), paddingProperty(), borderProperty()))
|
||||||
|
|
||||||
|
columns.forEach {
|
||||||
|
it.setPrefWidthPolicy(tableWidthWithoutPaddingAndBorder, getColumnWidth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <S> TableColumn<S, *>.setPrefWidthPolicy(
|
||||||
|
widthWithoutPaddingAndBorder: ObservableValue<Number>,
|
||||||
|
getColumnWidth: (tableWidthWithoutPaddingAndBorder: Number, column: TableColumn<S, *>) -> Number
|
||||||
|
) {
|
||||||
|
prefWidthProperty().bind(EasyBind.map(widthWithoutPaddingAndBorder) {
|
||||||
|
getColumnWidth(it, this)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <S, T> Formatter<T>.toTableCellFactory() = Callback<TableColumn<S, T?>, TableCell<S, T?>> {
|
||||||
|
object : TableCell<S, T?>() {
|
||||||
|
override fun updateItem(value: T?, empty: Boolean) {
|
||||||
|
super.updateItem(value, empty)
|
||||||
|
text = if (value == null || empty) {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
format(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <S> TableView<S>.singleRowSelection() = Bindings.createObjectBinding({
|
||||||
|
if (selectionModel.selectedItems.size == 0) {
|
||||||
|
SingleRowSelection.None<S>()
|
||||||
|
} else {
|
||||||
|
SingleRowSelection.Selected(selectionModel.selectedItems[0])
|
||||||
|
}
|
||||||
|
}, arrayOf(selectionModel.selectedItems))
|
||||||
|
|
||||||
|
fun <S, T> TableColumn<S, T>.setCustomCellFactory(toNode: (T) -> Node) {
|
||||||
|
setCellFactory {
|
||||||
|
object : TableCell<S, T>() {
|
||||||
|
init {
|
||||||
|
text = null
|
||||||
|
}
|
||||||
|
override fun updateItem(value: T?, empty: Boolean) {
|
||||||
|
super.updateItem(value, empty)
|
||||||
|
graphic = if (value != null && !empty) {
|
||||||
|
toNode(value)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,58 @@
|
|||||||
|
package com.r3corda.explorer.ui
|
||||||
|
|
||||||
|
import com.r3corda.explorer.formatters.Formatter
|
||||||
|
import javafx.beans.binding.Bindings
|
||||||
|
import javafx.beans.value.ObservableValue
|
||||||
|
import javafx.scene.control.TreeTableCell
|
||||||
|
import javafx.scene.control.TreeTableColumn
|
||||||
|
import javafx.scene.control.TreeTableView
|
||||||
|
import javafx.util.Callback
|
||||||
|
import org.fxmisc.easybind.EasyBind
|
||||||
|
|
||||||
|
|
||||||
|
fun <S> TreeTableView<S>.setColumnPrefWidthPolicy(
|
||||||
|
getColumnWidth: (tableWidthWithoutPaddingAndBorder: Number, column: TreeTableColumn<S, *>) -> Number
|
||||||
|
) {
|
||||||
|
val tableWidthWithoutPaddingAndBorder = Bindings.createDoubleBinding({
|
||||||
|
val padding = padding
|
||||||
|
val borderInsets = border?.insets
|
||||||
|
width -
|
||||||
|
(if (padding != null) padding.left + padding.right else 0.0) -
|
||||||
|
(if (borderInsets != null) borderInsets.left + borderInsets.right else 0.0)
|
||||||
|
}, arrayOf(columns, widthProperty(), paddingProperty(), borderProperty()))
|
||||||
|
|
||||||
|
columns.forEach {
|
||||||
|
it.setPrefWidthPolicy(tableWidthWithoutPaddingAndBorder, getColumnWidth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <S> TreeTableColumn<S, *>.setPrefWidthPolicy(
|
||||||
|
widthWithoutPaddingAndBorder: ObservableValue<Number>,
|
||||||
|
getColumnWidth: (tableWidthWithoutPaddingAndBorder: Number, column: TreeTableColumn<S, *>) -> Number
|
||||||
|
) {
|
||||||
|
prefWidthProperty().bind(EasyBind.map(widthWithoutPaddingAndBorder) {
|
||||||
|
getColumnWidth(it, this)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <S, T> Formatter<T>.toTreeTableCellFactory() = Callback<TreeTableColumn<S, T?>, TreeTableCell<S, T?>> {
|
||||||
|
object : TreeTableCell<S, T?>() {
|
||||||
|
override fun updateItem(value: T?, empty: Boolean) {
|
||||||
|
super.updateItem(value, empty)
|
||||||
|
text = if (value == null || empty) {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
format(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <S> TreeTableView<S>.singleRowSelection(): ObservableValue<out SingleRowSelection<S>> =
|
||||||
|
Bindings.createObjectBinding({
|
||||||
|
if (selectionModel.selectedItems.size == 0) {
|
||||||
|
SingleRowSelection.None<S>()
|
||||||
|
} else {
|
||||||
|
SingleRowSelection.Selected(selectionModel.selectedItems[0].value)
|
||||||
|
}
|
||||||
|
}, arrayOf(selectionModel.selectedItems))
|
@ -0,0 +1,364 @@
|
|||||||
|
package com.r3corda.explorer.views
|
||||||
|
|
||||||
|
import com.r3corda.client.fxutils.*
|
||||||
|
import com.r3corda.client.model.ContractStateModel
|
||||||
|
import com.r3corda.client.model.observableList
|
||||||
|
import com.r3corda.client.model.observableValue
|
||||||
|
import com.r3corda.contracts.asset.Cash
|
||||||
|
import com.r3corda.core.contracts.Amount
|
||||||
|
import com.r3corda.core.contracts.StateAndRef
|
||||||
|
import com.r3corda.core.contracts.withoutIssuer
|
||||||
|
import com.r3corda.core.crypto.Party
|
||||||
|
import com.r3corda.explorer.formatters.AmountFormatter
|
||||||
|
import com.r3corda.explorer.model.ReportingCurrencyModel
|
||||||
|
import com.r3corda.explorer.model.SettingsModel
|
||||||
|
import com.r3corda.explorer.ui.*
|
||||||
|
import javafx.beans.binding.Bindings
|
||||||
|
import javafx.beans.value.ObservableValue
|
||||||
|
import javafx.collections.FXCollections
|
||||||
|
import javafx.collections.ObservableList
|
||||||
|
import javafx.scene.Node
|
||||||
|
import javafx.scene.control.*
|
||||||
|
import javafx.scene.image.ImageView
|
||||||
|
import javafx.scene.input.MouseButton
|
||||||
|
import javafx.scene.input.MouseEvent
|
||||||
|
import javafx.scene.layout.HBox
|
||||||
|
import javafx.scene.layout.VBox
|
||||||
|
import org.fxmisc.easybind.EasyBind
|
||||||
|
import tornadofx.UIComponent
|
||||||
|
import tornadofx.View
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
sealed class FilterCriteria {
|
||||||
|
abstract fun matches(string: String): Boolean
|
||||||
|
|
||||||
|
object All : FilterCriteria() {
|
||||||
|
override fun matches(string: String) = true
|
||||||
|
}
|
||||||
|
|
||||||
|
class FilterString(val filterString: String) : FilterCriteria() {
|
||||||
|
override fun matches(string: String) = string.contains(filterString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CashViewer : View() {
|
||||||
|
// Inject UI elements
|
||||||
|
override val root: SplitPane by fxml()
|
||||||
|
|
||||||
|
val topSplitPane: SplitPane by fxid()
|
||||||
|
// Left pane
|
||||||
|
val leftPane: VBox by fxid()
|
||||||
|
val searchCriteriaTextField: TextField by fxid()
|
||||||
|
val searchCancelImageView: ImageView by fxid()
|
||||||
|
val totalMatchingLabel: Label by fxid()
|
||||||
|
val cashViewerTable: TreeTableView<ViewerNode> by fxid()
|
||||||
|
val cashViewerTableIssuerCurrency: TreeTableColumn<ViewerNode, String> by fxid()
|
||||||
|
val cashViewerTableLocalCurrency: TreeTableColumn<ViewerNode, Amount<Currency>?> by fxid()
|
||||||
|
val cashViewerTableEquiv: TreeTableColumn<ViewerNode, Amount<Currency>?> by fxid()
|
||||||
|
|
||||||
|
// Right pane
|
||||||
|
val rightPane: VBox by fxid()
|
||||||
|
val totalPositionsLabel: Label by fxid()
|
||||||
|
val equivSumLabel: Label by fxid()
|
||||||
|
val cashStatesList: ListView<StateRow> by fxid()
|
||||||
|
|
||||||
|
// Inject observables
|
||||||
|
val cashStates by observableList(ContractStateModel::cashStates)
|
||||||
|
val reportingCurrency: ObservableValue<Currency> by observableValue(SettingsModel::reportingCurrency)
|
||||||
|
val reportingExchange: ObservableValue<Pair<Currency, (Amount<Currency>) -> Amount<Currency>>>
|
||||||
|
by observableValue(ReportingCurrencyModel::reportingExchange)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This holds the data for each row in the TreeTable.
|
||||||
|
*/
|
||||||
|
sealed class ViewerNode {
|
||||||
|
object Root : ViewerNode()
|
||||||
|
class IssuerNode(
|
||||||
|
val issuer: Party,
|
||||||
|
val sumEquivAmount: ObservableValue<out Amount<Currency>>,
|
||||||
|
val states: ObservableList<StateAndRef<Cash.State>>
|
||||||
|
) : ViewerNode()
|
||||||
|
class CurrencyNode(
|
||||||
|
val amount: ObservableValue<Amount<Currency>>,
|
||||||
|
val equivAmount: ObservableValue<Amount<Currency>>,
|
||||||
|
val states: ObservableList<StateAndRef<Cash.State>>
|
||||||
|
) : ViewerNode()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We allow filtering by both issuer and currency. We do this by filtering by both at the same time and picking the
|
||||||
|
* one which produces more results, which seems to work, as the set of currency strings don't really overlap with
|
||||||
|
* issuer strings.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the filtering criterion based on the input text
|
||||||
|
*/
|
||||||
|
private val filterCriteria = searchCriteriaTextField.textProperty().map { text ->
|
||||||
|
if (text.isBlank()) {
|
||||||
|
FilterCriteria.All
|
||||||
|
} else {
|
||||||
|
FilterCriteria.FilterString(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter cash states based on issuer.
|
||||||
|
*/
|
||||||
|
private val issueFilteredCashStates = cashStates.filter(filterCriteria.map { criteria ->
|
||||||
|
{ state: StateAndRef<Cash.State> ->
|
||||||
|
criteria.matches(state.state.data.amount.token.issuer.party.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Now filter cash states based on currency.
|
||||||
|
*/
|
||||||
|
private val currencyFilteredCashStates = cashStates.filter(filterCriteria.map { criteria ->
|
||||||
|
{ state: StateAndRef<Cash.State> ->
|
||||||
|
criteria.matches(state.state.data.amount.token.product.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Now we pick which one to use.
|
||||||
|
*/
|
||||||
|
private val filteredCashStates = ChosenList(filterCriteria.map {
|
||||||
|
if (issueFilteredCashStates.size > currencyFilteredCashStates.size) {
|
||||||
|
issueFilteredCashStates
|
||||||
|
} else {
|
||||||
|
currencyFilteredCashStates
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is where we aggregate the list of cash states into the TreeTable structure.
|
||||||
|
*/
|
||||||
|
val cashViewerIssueNodes: ObservableList<TreeItem<out ViewerNode.IssuerNode>> =
|
||||||
|
/**
|
||||||
|
* First we group the states based on the issuer. [memberStates] is all states holding currency issued by [issuer]
|
||||||
|
*/
|
||||||
|
AggregatedList(filteredCashStates, { it.state.data.amount.token.issuer.party }) { issuer, memberStates ->
|
||||||
|
/**
|
||||||
|
* Next we create subgroups based on currency. [memberStates] here is all states holding currency [currency] issued by [issuer] above.
|
||||||
|
* Note that these states will not be displayed in the TreeTable, but rather in the side pane if the user clicks on the row.
|
||||||
|
*/
|
||||||
|
val currencyNodes = AggregatedList(memberStates, { it.state.data.amount.token.product }) { currency, memberStates ->
|
||||||
|
/**
|
||||||
|
* We sum the states in the subgroup, to be displayed in the "Local Currency" column
|
||||||
|
*/
|
||||||
|
val amounts = memberStates.map { it.state.data.amount.withoutIssuer() }
|
||||||
|
val sumAmount = amounts.fold(Amount(0, currency), Amount<Currency>::plus)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We exchange the sum to the reporting currency, to be displayed in the "<currency> Equiv" column.
|
||||||
|
*/
|
||||||
|
val equivSumAmount = EasyBind.combine(sumAmount, reportingExchange) { sum, exchange ->
|
||||||
|
exchange.second(sum)
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Finally assemble the actual TreeTable Currency node.
|
||||||
|
*/
|
||||||
|
TreeItem(ViewerNode.CurrencyNode(sumAmount, equivSumAmount, memberStates))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Now that we have all nodes per currency, we sum the exchanged amounts, to be displayed in the
|
||||||
|
* "<currency> Equiv" column, this time on the issuer level.
|
||||||
|
*/
|
||||||
|
val equivAmounts = currencyNodes.map { it.value.equivAmount }.flatten()
|
||||||
|
val equivSumAmount = reportingCurrency.bind { currency ->
|
||||||
|
equivAmounts.fold(Amount(0, currency), Amount<Currency>::plus)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assemble the Issuer node.
|
||||||
|
*/
|
||||||
|
val treeItem = TreeItem(ViewerNode.IssuerNode(issuer, equivSumAmount, memberStates))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind the children in the TreeTable structure.
|
||||||
|
*
|
||||||
|
* TODO Perhaps we shouldn't do this here, but rather have a generic way of binding nodes to the treetable once.
|
||||||
|
*/
|
||||||
|
treeItem.isExpanded = true
|
||||||
|
val children: List<TreeItem<out ViewerNode.IssuerNode>> = treeItem.children
|
||||||
|
Bindings.bindContent(children, currencyNodes)
|
||||||
|
treeItem
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Now we build up the Observables needed for the side pane, given that the user clicks on a row.
|
||||||
|
*/
|
||||||
|
val selectedViewerNode = cashViewerTable.singleRowSelection()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds data for a single state, to be displayed in the list in the side pane.
|
||||||
|
*/
|
||||||
|
data class StateRow (
|
||||||
|
val originated: LocalDateTime,
|
||||||
|
val stateAndRef: StateAndRef<Cash.State>
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A small class describing the graphics of a single state.
|
||||||
|
*/
|
||||||
|
inner class StateRowGraphic(
|
||||||
|
val stateRow: StateRow
|
||||||
|
) : UIComponent() {
|
||||||
|
override val root: HBox by fxml("CashStateViewer.fxml")
|
||||||
|
|
||||||
|
val equivLabel: Label by fxid()
|
||||||
|
val stateIdValueLabel: Label by fxid()
|
||||||
|
val issuerValueLabel: Label by fxid()
|
||||||
|
val originatedValueLabel: Label by fxid()
|
||||||
|
val amountValueLabel: Label by fxid()
|
||||||
|
val equivValueLabel: Label by fxid()
|
||||||
|
|
||||||
|
val equivAmount: ObservableValue<out Amount<Currency>> = reportingExchange.map {
|
||||||
|
it.second(stateRow.stateAndRef.state.data.amount.withoutIssuer())
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
val amountNoIssuer = stateRow.stateAndRef.state.data.amount.withoutIssuer()
|
||||||
|
val amountFormatter = AmountFormatter.boring
|
||||||
|
val equivFormatter = AmountFormatter.boring
|
||||||
|
|
||||||
|
equivLabel.textProperty().bind(equivAmount.map { it.token.currencyCode.toString() })
|
||||||
|
stateIdValueLabel.text = stateRow.stateAndRef.ref.toString()
|
||||||
|
issuerValueLabel.text = stateRow.stateAndRef.state.data.amount.token.issuer.toString()
|
||||||
|
originatedValueLabel.text = stateRow.originated.toString()
|
||||||
|
amountValueLabel.text = amountFormatter.format(amountNoIssuer)
|
||||||
|
equivValueLabel.textProperty().bind(equivAmount.map { equivFormatter.format(it) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of states related to the current selection. If none or the root is selected it's empty, if an issuer or
|
||||||
|
* currency node is selected it's the relevant states.
|
||||||
|
*/
|
||||||
|
private val noSelectionStates = FXCollections.observableArrayList<StateAndRef<Cash.State>>()
|
||||||
|
private val selectedViewerNodeStates = ChosenList(selectedViewerNode.map { selection ->
|
||||||
|
when (selection) {
|
||||||
|
is SingleRowSelection.None -> noSelectionStates
|
||||||
|
is SingleRowSelection.Selected ->
|
||||||
|
when (selection.node) {
|
||||||
|
CashViewer.ViewerNode.Root -> noSelectionStates
|
||||||
|
is CashViewer.ViewerNode.IssuerNode -> selection.node.states
|
||||||
|
is CashViewer.ViewerNode.CurrencyNode -> selection.node.states
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We re-display the exchanged sum amount, if we have a selection.
|
||||||
|
*/
|
||||||
|
private val noSelectionSumEquiv = reportingCurrency.map { Amount(0, it) }
|
||||||
|
private val selectedViewerNodeSumEquiv = selectedViewerNode.bind { selection ->
|
||||||
|
when (selection) {
|
||||||
|
is SingleRowSelection.None -> noSelectionSumEquiv
|
||||||
|
is SingleRowSelection.Selected ->
|
||||||
|
when (selection.node) {
|
||||||
|
ViewerNode.Root -> noSelectionSumEquiv
|
||||||
|
is ViewerNode.IssuerNode -> selection.node.sumEquivAmount
|
||||||
|
is ViewerNode.CurrencyNode -> selection.node.equivAmount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We add some extra timestamp data here to the selected states.
|
||||||
|
*
|
||||||
|
* TODO update this once we have actual timestamps.
|
||||||
|
*/
|
||||||
|
private val stateRows = selectedViewerNodeStates.map { StateRow(LocalDateTime.now(), it) }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We only display the right pane if a node is selected in the TreeTable.
|
||||||
|
*/
|
||||||
|
private val onlyLeftPaneShown = FXCollections.observableArrayList<Node>(leftPane)
|
||||||
|
private val bothPanesShown = FXCollections.observableArrayList<Node>(leftPane, rightPane)
|
||||||
|
private val panesShown = ChosenList<Node>(selectedViewerNode.map {
|
||||||
|
when (it) {
|
||||||
|
is SingleRowSelection.None -> onlyLeftPaneShown
|
||||||
|
is SingleRowSelection.Selected -> bothPanesShown
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Wire up UI
|
||||||
|
init {
|
||||||
|
searchCancelImageView.setOnMouseClicked { event: MouseEvent ->
|
||||||
|
if (event.button == MouseButton.PRIMARY) {
|
||||||
|
searchCriteriaTextField.text = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Bindings.bindContent(topSplitPane.items, panesShown)
|
||||||
|
|
||||||
|
totalPositionsLabel.textProperty().bind(Bindings.size(selectedViewerNodeStates).map {
|
||||||
|
val plural = if (it == 1) "" else "s"
|
||||||
|
"Total $it position$plural"
|
||||||
|
})
|
||||||
|
|
||||||
|
val equivSumLabelFormatter = AmountFormatter.boring
|
||||||
|
equivSumLabel.textProperty().bind(selectedViewerNodeSumEquiv.map {
|
||||||
|
equivSumLabelFormatter.format(it)
|
||||||
|
})
|
||||||
|
|
||||||
|
Bindings.bindContent(cashStatesList.items, stateRows)
|
||||||
|
|
||||||
|
cashStatesList.setCustomCellFactory { StateRowGraphic(it).root }
|
||||||
|
|
||||||
|
val cellFactory = AmountFormatter.boring.toTreeTableCellFactory<ViewerNode, Amount<Currency>>()
|
||||||
|
|
||||||
|
// TODO use smart resize
|
||||||
|
cashViewerTable.setColumnPrefWidthPolicy { tableWidthWithoutPaddingAndBorder, column ->
|
||||||
|
Math.floor(tableWidthWithoutPaddingAndBorder.toDouble() / cashViewerTable.columns.size).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
cashViewerTableIssuerCurrency.setCellValueFactory {
|
||||||
|
val node = it.value.value
|
||||||
|
when (node) {
|
||||||
|
ViewerNode.Root -> "".lift()
|
||||||
|
is ViewerNode.IssuerNode -> node.issuer.toString().lift()
|
||||||
|
is ViewerNode.CurrencyNode -> node.amount.map { it.token.toString() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cashViewerTableLocalCurrency.setCellValueFactory {
|
||||||
|
val node = it.value.value
|
||||||
|
when (node) {
|
||||||
|
ViewerNode.Root -> null.lift()
|
||||||
|
is ViewerNode.IssuerNode -> null.lift()
|
||||||
|
is ViewerNode.CurrencyNode -> node.amount.map { it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cashViewerTableLocalCurrency.cellFactory = cellFactory
|
||||||
|
/**
|
||||||
|
* We must set this, otherwise on sort an exception will be thrown, as it will try to compare Amounts of differing currency
|
||||||
|
*/
|
||||||
|
cashViewerTableLocalCurrency.isSortable = false
|
||||||
|
cashViewerTableEquiv.setCellValueFactory {
|
||||||
|
val node = it.value.value
|
||||||
|
when (node) {
|
||||||
|
ViewerNode.Root -> null.lift()
|
||||||
|
is ViewerNode.IssuerNode -> node.sumEquivAmount.map { it }
|
||||||
|
is ViewerNode.CurrencyNode -> node.equivAmount.map { it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cashViewerTableEquiv.cellFactory = cellFactory
|
||||||
|
cashViewerTableEquiv.textProperty().bind(reportingCurrency.map { "$it Equiv" })
|
||||||
|
|
||||||
|
cashViewerTable.root = TreeItem(ViewerNode.Root)
|
||||||
|
val children: List<TreeItem<out ViewerNode>> = cashViewerTable.root.children
|
||||||
|
Bindings.bindContent(children, cashViewerIssueNodes)
|
||||||
|
|
||||||
|
cashViewerTable.root.isExpanded = true
|
||||||
|
cashViewerTable.isShowRoot = false
|
||||||
|
|
||||||
|
// TODO Think about i18n!
|
||||||
|
totalMatchingLabel.textProperty().bind(Bindings.size(cashViewerIssueNodes).map {
|
||||||
|
val plural = if (it == 1) "" else "s"
|
||||||
|
"Total $it matching issuer$plural"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
package com.r3corda.explorer.views
|
||||||
|
|
||||||
|
import com.r3corda.client.model.observableValue
|
||||||
|
import com.r3corda.explorer.model.SelectedView
|
||||||
|
import com.r3corda.explorer.model.TopLevelModel
|
||||||
|
import javafx.beans.value.ObservableValue
|
||||||
|
import javafx.scene.control.Button
|
||||||
|
import javafx.scene.control.Label
|
||||||
|
import javafx.scene.image.Image
|
||||||
|
import javafx.scene.image.ImageView
|
||||||
|
import javafx.scene.layout.VBox
|
||||||
|
import org.fxmisc.easybind.EasyBind
|
||||||
|
import tornadofx.View
|
||||||
|
|
||||||
|
class Header : View() {
|
||||||
|
override val root: VBox by fxml()
|
||||||
|
|
||||||
|
private val sectionIcon: ImageView by fxid()
|
||||||
|
private val sectionIconContainer: VBox by fxid()
|
||||||
|
private val sectionLabel: Label by fxid()
|
||||||
|
private val debugNextButton: Button by fxid()
|
||||||
|
private val debugGoStopButton: Button by fxid()
|
||||||
|
|
||||||
|
private val selectedView: ObservableValue<SelectedView> by observableValue(TopLevelModel::selectedView)
|
||||||
|
|
||||||
|
private val homeImage = Image("/com/r3corda/explorer/images/home.png")
|
||||||
|
private val cashImage = Image("/com/r3corda/explorer/images/cash.png")
|
||||||
|
private val transactionImage = Image("/com/r3corda/explorer/images/tx.png")
|
||||||
|
|
||||||
|
init {
|
||||||
|
sectionLabel.textProperty().bind(EasyBind.map(selectedView) {
|
||||||
|
when (it) {
|
||||||
|
SelectedView.Home -> "Home"
|
||||||
|
SelectedView.Cash -> "Cash"
|
||||||
|
SelectedView.Transaction -> "Transactions"
|
||||||
|
null -> "Home"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
sectionIcon.imageProperty().bind(EasyBind.map(selectedView) {
|
||||||
|
when (it) {
|
||||||
|
SelectedView.Home -> homeImage
|
||||||
|
SelectedView.Cash -> cashImage
|
||||||
|
SelectedView.Transaction -> transactionImage
|
||||||
|
null -> homeImage
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// JavaFX bugs and doesn't invalidate the wrapping Box's height if the icon fit height is first set to
|
||||||
|
// unbounded (0.0) - which is what the label's height is initially, so we set it to 1.0 instead
|
||||||
|
val secionLabelHeightNonZero = EasyBind.map(sectionLabel.heightProperty()) {
|
||||||
|
if (it == 0.0) {
|
||||||
|
1.0
|
||||||
|
} else {
|
||||||
|
it.toDouble()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sectionIconContainer.minWidthProperty().bind(secionLabelHeightNonZero)
|
||||||
|
sectionIcon.fitWidthProperty().bind(secionLabelHeightNonZero)
|
||||||
|
sectionIcon.fitHeightProperty().bind(sectionIcon.fitWidthProperty())
|
||||||
|
}
|
||||||
|
}
|
68
explorer/src/main/kotlin/com/r3corda/explorer/views/Home.kt
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package com.r3corda.explorer.views
|
||||||
|
|
||||||
|
import com.r3corda.client.fxutils.AmountBindings
|
||||||
|
import com.r3corda.client.fxutils.map
|
||||||
|
import com.r3corda.client.model.*
|
||||||
|
import com.r3corda.contracts.asset.Cash
|
||||||
|
import com.r3corda.core.contracts.StateAndRef
|
||||||
|
import com.r3corda.core.contracts.withoutIssuer
|
||||||
|
import com.r3corda.explorer.formatters.AmountFormatter
|
||||||
|
import com.r3corda.explorer.model.SelectedView
|
||||||
|
import com.r3corda.explorer.model.SettingsModel
|
||||||
|
import com.r3corda.explorer.model.TopLevelModel
|
||||||
|
import javafx.beans.binding.Bindings
|
||||||
|
import javafx.beans.value.ObservableValue
|
||||||
|
import javafx.beans.value.WritableValue
|
||||||
|
import javafx.collections.ObservableList
|
||||||
|
import javafx.scene.control.Label
|
||||||
|
import javafx.scene.control.TitledPane
|
||||||
|
import javafx.scene.input.MouseButton
|
||||||
|
import javafx.scene.layout.TilePane
|
||||||
|
import org.fxmisc.easybind.EasyBind
|
||||||
|
import tornadofx.View
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
|
class Home : View() {
|
||||||
|
override val root: TilePane by fxml()
|
||||||
|
|
||||||
|
private val ourCashPane: TitledPane by fxid()
|
||||||
|
private val ourCashLabel: Label by fxid()
|
||||||
|
|
||||||
|
private val ourTransactionsPane: TitledPane by fxid()
|
||||||
|
private val ourTransactionsLabel: Label by fxid()
|
||||||
|
|
||||||
|
private val selectedView: WritableValue<SelectedView> by writableValue(TopLevelModel::selectedView)
|
||||||
|
private val cashStates: ObservableList<StateAndRef<Cash.State>> by observableList(ContractStateModel::cashStates)
|
||||||
|
private val gatheredTransactionDataList: ObservableList<out GatheredTransactionData>
|
||||||
|
by observableListReadOnly(GatheredTransactionDataModel::gatheredTransactionDataList)
|
||||||
|
private val reportingCurrency: ObservableValue<Currency> by observableValue(SettingsModel::reportingCurrency)
|
||||||
|
private val exchangeRate: ObservableValue<ExchangeRate> by observableValue(ExchangeRateModel::exchangeRate)
|
||||||
|
|
||||||
|
private val sumAmount = AmountBindings.sumAmountExchange(
|
||||||
|
cashStates.map { it.state.data.amount.withoutIssuer() },
|
||||||
|
reportingCurrency,
|
||||||
|
exchangeRate
|
||||||
|
)
|
||||||
|
|
||||||
|
init {
|
||||||
|
val formatter = AmountFormatter.boring
|
||||||
|
|
||||||
|
ourCashLabel.textProperty().bind(sumAmount.map { formatter.format(it) })
|
||||||
|
ourCashPane.setOnMouseClicked { clickEvent ->
|
||||||
|
if (clickEvent.button == MouseButton.PRIMARY) {
|
||||||
|
selectedView.value = SelectedView.Cash
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ourTransactionsLabel.textProperty().bind(
|
||||||
|
Bindings.size(gatheredTransactionDataList).map { it.toString() }
|
||||||
|
)
|
||||||
|
ourTransactionsPane.setOnMouseClicked { clickEvent ->
|
||||||
|
if (clickEvent.button == MouseButton.PRIMARY) {
|
||||||
|
selectedView.value = SelectedView.Transaction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
package com.r3corda.explorer.views
|
||||||
|
|
||||||
|
import com.r3corda.client.model.objectProperty
|
||||||
|
import com.r3corda.explorer.model.SelectedView
|
||||||
|
import com.r3corda.explorer.model.TopLevelModel
|
||||||
|
import javafx.beans.property.ObjectProperty
|
||||||
|
import javafx.scene.input.KeyCode
|
||||||
|
import javafx.scene.input.KeyEvent
|
||||||
|
import javafx.scene.layout.BorderPane
|
||||||
|
import javafx.scene.layout.Priority
|
||||||
|
import javafx.scene.layout.VBox
|
||||||
|
import org.fxmisc.easybind.EasyBind
|
||||||
|
import tornadofx.View
|
||||||
|
|
||||||
|
class TopLevel : View() {
|
||||||
|
override val root: VBox by fxml()
|
||||||
|
val selectionBorderPane: BorderPane by fxid()
|
||||||
|
|
||||||
|
private val header: Header by inject()
|
||||||
|
private val home: Home by inject()
|
||||||
|
private val cash: CashViewer by inject()
|
||||||
|
private val transaction: TransactionViewer by inject()
|
||||||
|
|
||||||
|
// Note: this is weirdly very important, as it forces the initialisation of Views. Therefore this is the entry
|
||||||
|
// point to the top level observable/stream wiring! Any events sent before this init may be lost!
|
||||||
|
private val homeRoot = home.root
|
||||||
|
private val cashRoot = cash.root
|
||||||
|
private val transactionRoot = transaction.root
|
||||||
|
|
||||||
|
private fun getView(selection: SelectedView) = when (selection) {
|
||||||
|
SelectedView.Home -> homeRoot
|
||||||
|
SelectedView.Cash -> cashRoot
|
||||||
|
SelectedView.Transaction -> transactionRoot
|
||||||
|
}
|
||||||
|
val selectedView: ObjectProperty<SelectedView> by objectProperty(TopLevelModel::selectedView)
|
||||||
|
|
||||||
|
init {
|
||||||
|
VBox.setVgrow(selectionBorderPane, Priority.ALWAYS)
|
||||||
|
selectionBorderPane.centerProperty().bind(EasyBind.map(selectedView) { getView(it) })
|
||||||
|
|
||||||
|
primaryStage.addEventHandler(KeyEvent.KEY_RELEASED) { keyEvent ->
|
||||||
|
if (keyEvent.code == KeyCode.ESCAPE) {
|
||||||
|
selectedView.value = SelectedView.Home
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
root.children.add(0, header.root)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,399 @@
|
|||||||
|
package com.r3corda.explorer.views
|
||||||
|
|
||||||
|
import com.r3corda.client.fxutils.ChosenList
|
||||||
|
import com.r3corda.client.fxutils.bind
|
||||||
|
import com.r3corda.client.fxutils.lift
|
||||||
|
import com.r3corda.client.fxutils.map
|
||||||
|
import com.r3corda.client.model.*
|
||||||
|
import com.r3corda.contracts.asset.Cash
|
||||||
|
import com.r3corda.core.contracts.*
|
||||||
|
import com.r3corda.core.crypto.Party
|
||||||
|
import com.r3corda.core.crypto.SecureHash
|
||||||
|
import com.r3corda.core.crypto.toStringShort
|
||||||
|
import com.r3corda.core.transactions.LedgerTransaction
|
||||||
|
import com.r3corda.explorer.AmountDiff
|
||||||
|
import com.r3corda.explorer.formatters.AmountFormatter
|
||||||
|
import com.r3corda.explorer.formatters.Formatter
|
||||||
|
import com.r3corda.explorer.formatters.NumberFormatter
|
||||||
|
import com.r3corda.explorer.model.IdentityModel
|
||||||
|
import com.r3corda.explorer.model.ReportingCurrencyModel
|
||||||
|
import com.r3corda.explorer.sign
|
||||||
|
import com.r3corda.explorer.ui.*
|
||||||
|
import com.r3corda.node.services.monitor.ServiceToClientEvent
|
||||||
|
import javafx.beans.binding.Bindings
|
||||||
|
import javafx.beans.value.ObservableValue
|
||||||
|
import javafx.collections.FXCollections
|
||||||
|
import javafx.collections.ObservableList
|
||||||
|
import javafx.geometry.Insets
|
||||||
|
import javafx.scene.Node
|
||||||
|
import javafx.scene.control.*
|
||||||
|
import javafx.scene.layout.Background
|
||||||
|
import javafx.scene.layout.BackgroundFill
|
||||||
|
import javafx.scene.layout.CornerRadii
|
||||||
|
import javafx.scene.layout.VBox
|
||||||
|
import javafx.scene.paint.Color
|
||||||
|
import org.fxmisc.easybind.EasyBind
|
||||||
|
import tornadofx.View
|
||||||
|
import java.security.PublicKey
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class TransactionViewer: View() {
|
||||||
|
override val root: VBox by fxml()
|
||||||
|
|
||||||
|
val topSplitPane: SplitPane by fxid()
|
||||||
|
|
||||||
|
// Top half (transactions table)
|
||||||
|
private val transactionViewTable: TableView<ViewerNode> by fxid()
|
||||||
|
private val transactionViewTransactionId: TableColumn<ViewerNode, String> by fxid()
|
||||||
|
private val transactionViewFiberId: TableColumn<ViewerNode, String> by fxid()
|
||||||
|
private val transactionViewClientUuid: TableColumn<ViewerNode, String> by fxid()
|
||||||
|
private val transactionViewTransactionStatus: TableColumn<ViewerNode, TransactionCreateStatus?> by fxid()
|
||||||
|
private val transactionViewProtocolStatus: TableColumn<ViewerNode, String> by fxid()
|
||||||
|
private val transactionViewStateMachineStatus: TableColumn<ViewerNode, StateMachineStatus?> by fxid()
|
||||||
|
private val transactionViewCommandTypes: TableColumn<ViewerNode, String> by fxid()
|
||||||
|
private val transactionViewTotalValueEquiv: TableColumn<ViewerNode, AmountDiff<Currency>> by fxid()
|
||||||
|
|
||||||
|
// Bottom half (details)
|
||||||
|
private val contractStatesTitledPane: TitledPane by fxid()
|
||||||
|
|
||||||
|
private val contractStatesInputsCountLabel: Label by fxid()
|
||||||
|
private val contractStatesInputStatesTable: TableView<StateNode> by fxid()
|
||||||
|
private val contractStatesInputStatesId: TableColumn<StateNode, String> by fxid()
|
||||||
|
private val contractStatesInputStatesType: TableColumn<StateNode, Class<out ContractState>> by fxid()
|
||||||
|
private val contractStatesInputStatesOwner: TableColumn<StateNode, String> by fxid()
|
||||||
|
private val contractStatesInputStatesLocalCurrency: TableColumn<StateNode, Currency?> by fxid()
|
||||||
|
private val contractStatesInputStatesAmount: TableColumn<StateNode, Long> by fxid()
|
||||||
|
private val contractStatesInputStatesEquiv: TableColumn<StateNode, Amount<Currency>> by fxid()
|
||||||
|
|
||||||
|
private val contractStatesOutputsCountLabel: Label by fxid()
|
||||||
|
private val contractStatesOutputStatesTable: TableView<StateNode> by fxid()
|
||||||
|
private val contractStatesOutputStatesId: TableColumn<StateNode, String> by fxid()
|
||||||
|
private val contractStatesOutputStatesType: TableColumn<StateNode, Class<out ContractState>> by fxid()
|
||||||
|
private val contractStatesOutputStatesOwner: TableColumn<StateNode, String> by fxid()
|
||||||
|
private val contractStatesOutputStatesLocalCurrency: TableColumn<StateNode, Currency?> by fxid()
|
||||||
|
private val contractStatesOutputStatesAmount: TableColumn<StateNode, Long> by fxid()
|
||||||
|
private val contractStatesOutputStatesEquiv: TableColumn<StateNode, Amount<Currency>> by fxid()
|
||||||
|
|
||||||
|
private val signaturesTitledPane: TitledPane by fxid()
|
||||||
|
private val signaturesList: ListView<PublicKey> by fxid()
|
||||||
|
|
||||||
|
private val lowLevelEventsTitledPane: TitledPane by fxid()
|
||||||
|
private val lowLevelEventsTable: TableView<ServiceToClientEvent> by fxid()
|
||||||
|
private val lowLevelEventsTimestamp: TableColumn<ServiceToClientEvent, Instant> by fxid()
|
||||||
|
private val lowLevelEventsEvent: TableColumn<ServiceToClientEvent, ServiceToClientEvent> by fxid()
|
||||||
|
|
||||||
|
private val matchingTransactionsLabel: Label by fxid()
|
||||||
|
|
||||||
|
// Inject data
|
||||||
|
private val gatheredTransactionDataList: ObservableList<out GatheredTransactionData>
|
||||||
|
by observableListReadOnly(GatheredTransactionDataModel::gatheredTransactionDataList)
|
||||||
|
private val reportingExchange: ObservableValue<Pair<Currency, (Amount<Currency>) -> Amount<Currency>>>
|
||||||
|
by observableValue(ReportingCurrencyModel::reportingExchange)
|
||||||
|
private val myIdentity: ObservableValue<Party> by observableValue(IdentityModel::myIdentity)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is what holds data for a single transaction node. Note how a lot of these are nullable as we often simply don't
|
||||||
|
* have the data.
|
||||||
|
*/
|
||||||
|
data class ViewerNode(
|
||||||
|
val transactionId: ObservableValue<SecureHash?>,
|
||||||
|
val fiberId: ObservableValue<Long?>,
|
||||||
|
val clientUuid: ObservableValue<UUID?>,
|
||||||
|
val originator: ObservableValue<String>,
|
||||||
|
val transactionStatus: ObservableValue<TransactionCreateStatus?>,
|
||||||
|
val stateMachineStatus: ObservableValue<StateMachineStatus?>,
|
||||||
|
val protocolStatus: ObservableValue<ProtocolStatus?>,
|
||||||
|
val statusUpdated: ObservableValue<Instant>,
|
||||||
|
val commandTypes: ObservableValue<Collection<Class<CommandData>>>,
|
||||||
|
val totalValueEquiv: ObservableValue<AmountDiff<Currency>?>,
|
||||||
|
val transaction: ObservableValue<LedgerTransaction?>,
|
||||||
|
val allEvents: ObservableList<out ServiceToClientEvent>
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds information about a single input/output state, to be displayed in the [contractStatesTitledPane]
|
||||||
|
*/
|
||||||
|
data class StateNode(
|
||||||
|
val transactionState: TransactionState<*>,
|
||||||
|
val stateRef: StateRef
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We map the gathered data about transactions almost one-to-one to the nodes.
|
||||||
|
*/
|
||||||
|
private val viewerNodes = gatheredTransactionDataList.map {
|
||||||
|
ViewerNode(
|
||||||
|
transactionId = it.transaction.map { it?.id },
|
||||||
|
fiberId = it.fiberId,
|
||||||
|
clientUuid = it.uuid,
|
||||||
|
/**
|
||||||
|
* We can't really do any better based on uuid, we need to store explicit data for this TODO
|
||||||
|
*/
|
||||||
|
originator = it.uuid.map { uuid ->
|
||||||
|
if (uuid == null) {
|
||||||
|
"Someone"
|
||||||
|
} else {
|
||||||
|
"Us"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
transactionStatus = it.status,
|
||||||
|
protocolStatus = it.protocolStatus,
|
||||||
|
stateMachineStatus = it.stateMachineStatus,
|
||||||
|
statusUpdated = it.lastUpdate,
|
||||||
|
commandTypes = it.transaction.map {
|
||||||
|
val commands = mutableSetOf<Class<CommandData>>()
|
||||||
|
it?.commands?.forEach {
|
||||||
|
commands.add(it.value.javaClass)
|
||||||
|
}
|
||||||
|
commands
|
||||||
|
},
|
||||||
|
totalValueEquiv = ::calculateTotalEquiv.lift(myIdentity, reportingExchange, it.transaction),
|
||||||
|
transaction = it.transaction,
|
||||||
|
allEvents = it.allEvents
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The detail panes are only filled out if a transaction is selected
|
||||||
|
*/
|
||||||
|
private val selectedViewerNode = transactionViewTable.singleRowSelection()
|
||||||
|
private val selectedTransaction = selectedViewerNode.bind {
|
||||||
|
when (it) {
|
||||||
|
is SingleRowSelection.None -> null.lift()
|
||||||
|
is SingleRowSelection.Selected -> it.node.transaction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val inputStateNodes = ChosenList(selectedTransaction.map {
|
||||||
|
if (it == null) {
|
||||||
|
FXCollections.emptyObservableList<StateNode>()
|
||||||
|
} else {
|
||||||
|
FXCollections.observableArrayList(it.inputs.map { StateNode(it.state, it.ref) })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
private val outputStateNodes = ChosenList(selectedTransaction.map {
|
||||||
|
if (it == null) {
|
||||||
|
FXCollections.emptyObservableList<StateNode>()
|
||||||
|
} else {
|
||||||
|
FXCollections.observableArrayList(it.outputs.mapIndexed { index, transactionState ->
|
||||||
|
StateNode(transactionState, StateRef(it.id, index))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
private val signatures = ChosenList(selectedTransaction.map {
|
||||||
|
if (it == null) {
|
||||||
|
FXCollections.emptyObservableList<PublicKey>()
|
||||||
|
} else {
|
||||||
|
FXCollections.observableArrayList(it.mustSign)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
private val lowLevelEvents = ChosenList(selectedViewerNode.map {
|
||||||
|
when (it) {
|
||||||
|
is SingleRowSelection.None -> FXCollections.emptyObservableList<ServiceToClientEvent>()
|
||||||
|
is SingleRowSelection.Selected -> it.node.allEvents
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We only display the detail panes if there is a node selected.
|
||||||
|
*/
|
||||||
|
private val allNodesShown = FXCollections.observableArrayList<Node>(
|
||||||
|
transactionViewTable,
|
||||||
|
contractStatesTitledPane,
|
||||||
|
signaturesTitledPane,
|
||||||
|
lowLevelEventsTitledPane
|
||||||
|
)
|
||||||
|
private val onlyTransactionsTableShown = FXCollections.observableArrayList<Node>(
|
||||||
|
transactionViewTable
|
||||||
|
)
|
||||||
|
private val topSplitPaneNodesShown = ChosenList(
|
||||||
|
selectedViewerNode.map { selection ->
|
||||||
|
if (selection is SingleRowSelection.None<*>) {
|
||||||
|
onlyTransactionsTableShown
|
||||||
|
} else {
|
||||||
|
allNodesShown
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Both input and output state tables look the same, so we each up with [wireUpStatesTable]
|
||||||
|
*/
|
||||||
|
private fun wireUpStatesTable(
|
||||||
|
states: ObservableList<StateNode>,
|
||||||
|
statesCountLabel: Label,
|
||||||
|
statesTable: TableView<StateNode>,
|
||||||
|
statesId: TableColumn<StateNode, String>,
|
||||||
|
statesType: TableColumn<StateNode, Class<out ContractState>>,
|
||||||
|
statesOwner: TableColumn<StateNode, String>,
|
||||||
|
statesLocalCurrency: TableColumn<StateNode, Currency?>,
|
||||||
|
statesAmount: TableColumn<StateNode, Long>,
|
||||||
|
statesEquiv: TableColumn<StateNode, Amount<Currency>>
|
||||||
|
) {
|
||||||
|
statesCountLabel.textProperty().bind(Bindings.size(states).map { "$it" })
|
||||||
|
|
||||||
|
Bindings.bindContent(statesTable.items, states)
|
||||||
|
|
||||||
|
statesId.setCellValueFactory { it.value.stateRef.toString().lift() }
|
||||||
|
statesType.setCellValueFactory { it.value.transactionState.data.javaClass.lift() }
|
||||||
|
statesOwner.setCellValueFactory {
|
||||||
|
val state = it.value.transactionState.data
|
||||||
|
if (state is OwnableState) {
|
||||||
|
state.owner.toStringShort().lift()
|
||||||
|
} else {
|
||||||
|
"???".lift()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
statesLocalCurrency.setCellValueFactory {
|
||||||
|
val state = it.value.transactionState.data
|
||||||
|
if (state is Cash.State) {
|
||||||
|
state.amount.token.product.lift()
|
||||||
|
} else {
|
||||||
|
null.lift()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
statesAmount.setCellValueFactory {
|
||||||
|
val state = it.value.transactionState.data
|
||||||
|
if (state is Cash.State) {
|
||||||
|
state.amount.quantity.lift()
|
||||||
|
} else {
|
||||||
|
null.lift()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
statesAmount.cellFactory = NumberFormatter.boringLong.toTableCellFactory()
|
||||||
|
statesEquiv.setCellValueFactory {
|
||||||
|
val state = it.value.transactionState.data
|
||||||
|
if (state is Cash.State) {
|
||||||
|
reportingExchange.map { exchange ->
|
||||||
|
exchange.second(state.amount.withoutIssuer())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null.lift()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
statesEquiv.cellFactory = AmountFormatter.boring.toTableCellFactory()
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
Bindings.bindContent(topSplitPane.items, topSplitPaneNodesShown)
|
||||||
|
|
||||||
|
// Transaction table
|
||||||
|
Bindings.bindContent(transactionViewTable.items, viewerNodes)
|
||||||
|
|
||||||
|
transactionViewTable.setColumnPrefWidthPolicy { tableWidthWithoutPaddingAndBorder, column ->
|
||||||
|
Math.floor(tableWidthWithoutPaddingAndBorder.toDouble() / transactionViewTable.columns.size).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
transactionViewTransactionId.setCellValueFactory { it.value.transactionId.map { "${it ?: ""}" } }
|
||||||
|
transactionViewFiberId.setCellValueFactory { it.value.fiberId.map { "${it?: ""}" } }
|
||||||
|
transactionViewClientUuid.setCellValueFactory { it.value.clientUuid.map { "${it ?: ""}" } }
|
||||||
|
transactionViewProtocolStatus.setCellValueFactory { it.value.protocolStatus.map { "${it ?: ""}" } }
|
||||||
|
transactionViewTransactionStatus.setCellValueFactory { it.value.transactionStatus }
|
||||||
|
transactionViewTransactionStatus.setCustomCellFactory {
|
||||||
|
val label = Label()
|
||||||
|
val backgroundFill = when (it) {
|
||||||
|
is TransactionCreateStatus.Started -> BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY)
|
||||||
|
is TransactionCreateStatus.Failed -> BackgroundFill(Color.SALMON, CornerRadii.EMPTY, Insets.EMPTY)
|
||||||
|
null -> BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY)
|
||||||
|
}
|
||||||
|
label.background = Background(backgroundFill)
|
||||||
|
label.text = "$it"
|
||||||
|
label
|
||||||
|
}
|
||||||
|
transactionViewStateMachineStatus.setCellValueFactory { it.value.stateMachineStatus }
|
||||||
|
transactionViewStateMachineStatus.setCustomCellFactory {
|
||||||
|
val label = Label()
|
||||||
|
val backgroundFill = when (it) {
|
||||||
|
is StateMachineStatus.Added -> BackgroundFill(Color.LIGHTYELLOW, CornerRadii.EMPTY, Insets.EMPTY)
|
||||||
|
is StateMachineStatus.Removed -> BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY)
|
||||||
|
null -> BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY)
|
||||||
|
}
|
||||||
|
label.background = Background(backgroundFill)
|
||||||
|
label.text = "$it"
|
||||||
|
label
|
||||||
|
}
|
||||||
|
|
||||||
|
transactionViewCommandTypes.setCellValueFactory {
|
||||||
|
it.value.commandTypes.map { it.map { it.simpleName }.joinToString(",") }
|
||||||
|
}
|
||||||
|
transactionViewTotalValueEquiv.setCellValueFactory<ViewerNode, AmountDiff<Currency>> { it.value.totalValueEquiv }
|
||||||
|
transactionViewTotalValueEquiv.cellFactory = object : Formatter<AmountDiff<Currency>> {
|
||||||
|
override fun format(value: AmountDiff<Currency>) =
|
||||||
|
"${value.positivity.sign}${AmountFormatter.boring.format(value.amount)}"
|
||||||
|
}.toTableCellFactory()
|
||||||
|
|
||||||
|
// Contract states
|
||||||
|
wireUpStatesTable(
|
||||||
|
inputStateNodes,
|
||||||
|
contractStatesInputsCountLabel,
|
||||||
|
contractStatesInputStatesTable,
|
||||||
|
contractStatesInputStatesId,
|
||||||
|
contractStatesInputStatesType,
|
||||||
|
contractStatesInputStatesOwner,
|
||||||
|
contractStatesInputStatesLocalCurrency,
|
||||||
|
contractStatesInputStatesAmount,
|
||||||
|
contractStatesInputStatesEquiv
|
||||||
|
)
|
||||||
|
wireUpStatesTable(
|
||||||
|
outputStateNodes,
|
||||||
|
contractStatesOutputsCountLabel,
|
||||||
|
contractStatesOutputStatesTable,
|
||||||
|
contractStatesOutputStatesId,
|
||||||
|
contractStatesOutputStatesType,
|
||||||
|
contractStatesOutputStatesOwner,
|
||||||
|
contractStatesOutputStatesLocalCurrency,
|
||||||
|
contractStatesOutputStatesAmount,
|
||||||
|
contractStatesOutputStatesEquiv
|
||||||
|
)
|
||||||
|
|
||||||
|
// Signatures
|
||||||
|
Bindings.bindContent(signaturesList.items, signatures)
|
||||||
|
signaturesList.cellFactory = object : Formatter<PublicKey> {
|
||||||
|
override fun format(value: PublicKey) = value.toStringShort()
|
||||||
|
}.toListCellFactory()
|
||||||
|
|
||||||
|
// Low level events
|
||||||
|
Bindings.bindContent(lowLevelEventsTable.items, lowLevelEvents)
|
||||||
|
lowLevelEventsTimestamp.setCellValueFactory { it.value.time.lift() }
|
||||||
|
lowLevelEventsEvent.setCellValueFactory { it.value.lift() }
|
||||||
|
lowLevelEventsTable.setColumnPrefWidthPolicy { tableWidthWithoutPaddingAndBorder, column ->
|
||||||
|
Math.floor(tableWidthWithoutPaddingAndBorder.toDouble() / lowLevelEventsTable.columns.size).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
matchingTransactionsLabel.textProperty().bind(EasyBind.map(Bindings.size(viewerNodes)) {
|
||||||
|
"$it matching transaction${if (it == 1) "" else "s"}"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We calculate the total value by subtracting relevant input states and adding relevant output states, as long as they're cash
|
||||||
|
*/
|
||||||
|
private fun calculateTotalEquiv(
|
||||||
|
identity: Party,
|
||||||
|
reportingCurrencyExchange: Pair<Currency, (Amount<Currency>) -> Amount<Currency>>,
|
||||||
|
transaction: LedgerTransaction?): AmountDiff<Currency>? {
|
||||||
|
if (transaction == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
var sum = 0L
|
||||||
|
val (reportingCurrency, exchange) = reportingCurrencyExchange
|
||||||
|
val publicKey = identity.owningKey
|
||||||
|
transaction.inputs.forEach {
|
||||||
|
val contractState = it.state.data
|
||||||
|
if (contractState is Cash.State && publicKey == contractState.owner) {
|
||||||
|
sum -= exchange(contractState.amount.withoutIssuer()).quantity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
transaction.outputs.forEach {
|
||||||
|
val contractState = it.data
|
||||||
|
if (contractState is Cash.State && publicKey == contractState.owner) {
|
||||||
|
sum += exchange(contractState.amount.withoutIssuer()).quantity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return AmountDiff.fromLong(sum, reportingCurrency)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
|||||||
|
.notification-bar {
|
||||||
|
-fx-background-color: linear-gradient(to bottom, black, darkslategray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bar > .notification-bar-item {
|
||||||
|
-fx-padding: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bar > .notification-bar-item > Label {
|
||||||
|
-fx-text-fill: white;
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-font-size: 13;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bar > .notification-bar-item > .progress-bar > .bar {
|
||||||
|
-fx-padding: 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bar > .notification-bar-item > .progress-bar > .track {
|
||||||
|
-fx-opacity: 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-bar > .notification-bar-item > .button {
|
||||||
|
-fx-base: orange;
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-font-size: 12;
|
||||||
|
-fx-text-fill: white;
|
||||||
|
-fx-background-insets: 1;
|
||||||
|
-fx-background-radius: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thin-progress-bar > .bar {
|
||||||
|
-fx-padding: 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thin-progress-bar > .track {
|
||||||
|
-fx-background-color: #bce7f5;
|
||||||
|
-fx-background-insets: 3 3 4 3;
|
||||||
|
/*-fx-background-radius: 0.583em; *//* 7 */
|
||||||
|
-fx-background-radius: 2;
|
||||||
|
-fx-padding: 8;
|
||||||
|
}
|
143
explorer/src/main/resources/com/r3corda/explorer/css/shell.css
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
.root {
|
||||||
|
-fx-font-family: "Roboto Light", sans-serif;
|
||||||
|
-fx-font-size: 12pt;
|
||||||
|
/* Setting the background colour explicitly is required for a correct fade/blur animation. */
|
||||||
|
-fx-background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
-fx-background-color: #494949;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-right-shadow {
|
||||||
|
-fx-background-color: linear-gradient(to left, #1c1c1c, #494949);
|
||||||
|
-fx-border-color: black;
|
||||||
|
-fx-border-width: 0 0.1em 0 0;
|
||||||
|
-fx-padding: 0 0.2em 0 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-button {
|
||||||
|
-fx-base: transparent;
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
-fx-text-fill: white;
|
||||||
|
-fx-graphic-text-gap: 1em;
|
||||||
|
-fx-cursor: hand;
|
||||||
|
-fx-effect: dropshadow(three-pass-box, black, 6, 0.0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-button:selected {
|
||||||
|
-fx-background-color: grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-icon {
|
||||||
|
-fx-font-size: 25pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-window {
|
||||||
|
-fx-background-color: white;
|
||||||
|
-fx-background-radius: 5;
|
||||||
|
-fx-effect: dropshadow(three-pass-box, black, 10, 0.0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-window > .title-bar {
|
||||||
|
-fx-background-radius: 5 5 0 0;
|
||||||
|
-fx-background-color: #3c777b;
|
||||||
|
-fx-alignment: center-left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-window > .title-bar > Button {
|
||||||
|
-fx-base: transparent;
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
-fx-cursor: hand;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-window > .title-bar > Button:hover {
|
||||||
|
-fx-base: #3c777b;
|
||||||
|
-fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color;
|
||||||
|
-fx-text-fill: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-window > .title-bar > Label {
|
||||||
|
-fx-text-fill: white;
|
||||||
|
-fx-font-size: 120%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.large-font {
|
||||||
|
-fx-font-size: 200%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/********************************************************************************************************************
|
||||||
|
*
|
||||||
|
* Buttons
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
.flat-button {
|
||||||
|
-fx-background-color: white;
|
||||||
|
-fx-padding: 0 0 0 0;
|
||||||
|
-fx-font-size: 10pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flat-button:hover {
|
||||||
|
-fx-underline: true;
|
||||||
|
-fx-cursor: hand;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flat-button:focused {
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fat-buttons {
|
||||||
|
-fx-spacing: 15.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fat-buttons Button {
|
||||||
|
-fx-padding: 10 15 10 15;
|
||||||
|
-fx-min-width: 100;
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
-fx-base: whitesmoke;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fat-buttons Button:default {
|
||||||
|
-fx-base: orange;
|
||||||
|
-fx-text-fill: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fat-buttons Button:cancel {
|
||||||
|
-fx-background-color: white;
|
||||||
|
-fx-background-insets: 1;
|
||||||
|
-fx-border-color: lightgray;
|
||||||
|
-fx-border-radius: 3;
|
||||||
|
-fx-text-fill: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fat-buttons Button:cancel:hover {
|
||||||
|
-fx-base: white;
|
||||||
|
-fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color;
|
||||||
|
-fx-text-fill: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** take out the focus ring */
|
||||||
|
.no-focus-button:focused {
|
||||||
|
-fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color;
|
||||||
|
-fx-background-insets: 0 0 -1 0, 0, 1, 2;
|
||||||
|
-fx-background-radius: 3px, 3px, 2px, 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blue-button {
|
||||||
|
-fx-base: lightblue;
|
||||||
|
-fx-text-fill: darkslategrey;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blue-button:disabled {
|
||||||
|
-fx-text-fill: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.green-button {
|
||||||
|
-fx-base: #62c462;
|
||||||
|
-fx-text-fill: darkslategrey;
|
||||||
|
}
|
||||||
|
|
||||||
|
.green-button:disabled {
|
||||||
|
-fx-text-fill: white;
|
||||||
|
}
|
400
explorer/src/main/resources/com/r3corda/explorer/css/wallet.css
Normal file
@ -0,0 +1,400 @@
|
|||||||
|
#topLevel.root {
|
||||||
|
-fx-background-image:url('../images/r3bg.png');
|
||||||
|
-fx-background-size: cover;
|
||||||
|
-fx-background-repeat:no-repeat;
|
||||||
|
-fx-base:white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#cashViewer {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
#cashViewerTable {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
#cashViewerTable .tree-table-row-cell {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root {
|
||||||
|
-fx-padding:5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root {
|
||||||
|
-fx-padding:5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-pane {
|
||||||
|
-fx-background-color:rgba(255,255,255,0.7);
|
||||||
|
-fx-background-radius:2px;
|
||||||
|
-fx-border-radius: 2px;
|
||||||
|
-fx-border-color: rgb(20,136,204);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.nested-column-header, .nested-column-header {
|
||||||
|
-fx-background-color:transparent;
|
||||||
|
-fx-wrap-text:true;
|
||||||
|
-fx-border-color:transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.text-field,
|
||||||
|
.table-column,
|
||||||
|
.label,
|
||||||
|
.title,
|
||||||
|
.combo-box,
|
||||||
|
.button,
|
||||||
|
.split-menu-button,
|
||||||
|
.choice-box {
|
||||||
|
-fx-font-family:Effra;
|
||||||
|
-fx-font-size:1em;
|
||||||
|
-fx-text-fill:rgb(63,63,63);
|
||||||
|
-fx-font-smoothing-type: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-highlight {
|
||||||
|
-fx-text-fill:rgb(20,136,204);
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu {
|
||||||
|
-fx-background-color:rgba(255,255,255,0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.titled-pane .content,
|
||||||
|
.split-menu-button .label,
|
||||||
|
.split-menu-button .arrow-button,
|
||||||
|
.titled-pane .split-pane, .scroll-pane {
|
||||||
|
-fx-background-color:transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.text-field,
|
||||||
|
.tree-table-view,
|
||||||
|
.table-view,
|
||||||
|
.accordion,
|
||||||
|
.combo-box,
|
||||||
|
.context-menu,
|
||||||
|
.button,
|
||||||
|
.split-menu-button,
|
||||||
|
.choice-box,
|
||||||
|
.titled-pane .title {
|
||||||
|
-fx-border-color:rgb(150,150,150);
|
||||||
|
-fx-border-width:1px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-field:focused,
|
||||||
|
.tree-table-view:focused,
|
||||||
|
.table-view:focused,
|
||||||
|
.accordion:focused,
|
||||||
|
.combo-box:focused,
|
||||||
|
.context-menu:focused,
|
||||||
|
.button:focused,
|
||||||
|
.split-menu-button:focused,
|
||||||
|
.text-field:hover,
|
||||||
|
.button:hover,
|
||||||
|
.split-menu-button:hover,
|
||||||
|
.choice-box:hover,
|
||||||
|
.titled-pane:hover .title {
|
||||||
|
-fx-border-color:rgb(20,136,204);
|
||||||
|
}
|
||||||
|
|
||||||
|
.split-menu-button:pressed,
|
||||||
|
.button:pressed,
|
||||||
|
.choice-box:pressed,
|
||||||
|
.titled-pane:expanded .title {
|
||||||
|
-fx-background-color:rgb(20,136,204);
|
||||||
|
-fx-text-fill:white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.titled-pane:expanded .title .text {
|
||||||
|
-fx-fill:white;
|
||||||
|
}
|
||||||
|
.titled-pane .title,.titled-pane .title:hover {
|
||||||
|
-fx-border-width:0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-field, .combo-box, .choice-box, .password-field {
|
||||||
|
-fx-background-color:rgba(255,255,255,0.5);
|
||||||
|
-fx-background-radius:2px;
|
||||||
|
-fx-border-radius: 2px;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* switch off highlighting for text-field when it's inside a combo-box */
|
||||||
|
.combo-box .text-field, .combo-box .text-field:hover, .combo-box .text-field:focused {
|
||||||
|
-fx-border-color:transparent;
|
||||||
|
}
|
||||||
|
/* table formatting */
|
||||||
|
|
||||||
|
.column-header-background,
|
||||||
|
.table-column,
|
||||||
|
.tree-table-row-cell, .column-header-background .filler {
|
||||||
|
-fx-background-color:transparent;
|
||||||
|
-fx-label-padding:3px;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.nested-column-header .label { -fx-wrap-text:true}
|
||||||
|
|
||||||
|
.table-column { -fx-border-style:solid;
|
||||||
|
-fx-border-color:rgb(216,216,216); /*t r b l */
|
||||||
|
|
||||||
|
-fx-border-width:0.5px;
|
||||||
|
-fx-border-insets: 1.5px;
|
||||||
|
-fx-background-insets: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.tree-table-row-cell:even .table-column,
|
||||||
|
.table-row-cell:even .table-column,
|
||||||
|
.title, .split-menu-button, .button {
|
||||||
|
-fx-background-color: rgba(20,136,204,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row-cell:selected, .tree-table-row-cell:selected {
|
||||||
|
-fx-background-color:transparent;
|
||||||
|
}
|
||||||
|
.tree-table-row-cell:selected .table-column,
|
||||||
|
.tree-table-row-cell:focused .table-column,
|
||||||
|
.table-row-cell:selected .table-column,
|
||||||
|
.table-row-cell:focused .table-column {
|
||||||
|
-fx-background-color:rgba(20,136,204,0.8);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
.bad .text {
|
||||||
|
-fx-fill:rgb(236,29,36);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row-cell:focused .table-column .text,
|
||||||
|
.tree-table-row-cell:focused .table-column .text {
|
||||||
|
-fx-fill:white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-column:hover,
|
||||||
|
.table-row-cell:hover .first-column,
|
||||||
|
.table-row-cell:hover .second-column,
|
||||||
|
.tree-table-row-cell:hover .first-column,
|
||||||
|
.tree-table-row-cell:hover .second-column {
|
||||||
|
-fx-border-color:rgb(20,136,204);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-table-view .column-header-background .nested-column-header .table-column,
|
||||||
|
.table-view .column-header-background .nested-column-header .table-column {
|
||||||
|
-fx-border-color:transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Special formatting - columns to be presented with no join between them */
|
||||||
|
|
||||||
|
.first-column {
|
||||||
|
-fx-border-width:0.5px 0px 0.5px 0.5px;
|
||||||
|
-fx-border-insets: 1.5px 0px 1.5px 1.5px;
|
||||||
|
-fx-background-insets: 2px 0px 2px 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.second-column {
|
||||||
|
-fx-border-width:0.5px 0.5px 0.5px 0px;
|
||||||
|
-fx-border-insets: 1.5px 1.5px 1.5px 0px;
|
||||||
|
-fx-background-insets: 2px 2px 2px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* highlighting where the user has typed a key */
|
||||||
|
.tree-table-view text-area, .table-view text-area{
|
||||||
|
-fx-font-weight:bold;
|
||||||
|
-fx-fill:rgb(20,136,204);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-table-row-cell:selected .table-column text-area,
|
||||||
|
.table-row-cell:selected .table-column text-area
|
||||||
|
{
|
||||||
|
-fx-font-weight:bold;
|
||||||
|
-fx-fill:rgb(255,255,255);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* labels */
|
||||||
|
.dialog-pane .header-panel .label .text{
|
||||||
|
-fx-font-size:1em;
|
||||||
|
-fx-fill:rgb(20,136,204);
|
||||||
|
}
|
||||||
|
|
||||||
|
#headline, .headline {
|
||||||
|
-fx-font-size:2.4em;
|
||||||
|
}
|
||||||
|
#subline, .subline {
|
||||||
|
-fx-font-size:1.4em;
|
||||||
|
}
|
||||||
|
#headline, #subline {
|
||||||
|
-fx-text-fill:rgb(65,65,65);
|
||||||
|
-fx-padding:0px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* search boxes */
|
||||||
|
.search {
|
||||||
|
-fx-background-image:url('../images/search.png');
|
||||||
|
-fx-background-size:Auto 16px;
|
||||||
|
-fx-background-repeat:no-repeat;
|
||||||
|
-fx-background-position:8px center;
|
||||||
|
-fx-padding:5px 5px 5px 30px;
|
||||||
|
-fx-background-radius: 2px;
|
||||||
|
-fx-border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-clear {
|
||||||
|
-fx-image:url('../images/clear_inactive.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-clear:hover {
|
||||||
|
-fx-image:url('../images/clear.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
.split-menu-button, .button, .choice-box {
|
||||||
|
-fx-background-radius:2px;
|
||||||
|
-fx-border-radius: 2px;
|
||||||
|
-fx-border-insets: 0.5px;
|
||||||
|
-fx-background-insets:0.5px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-table-row-cell .monetary-value, .monetary-value .label, .table-row-cell .monetary-value {
|
||||||
|
-fx-alignment:center-right;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* split panes */
|
||||||
|
.split-pane-divider {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
-fx-border-color: rgb(160,160,160);
|
||||||
|
-fx-border-width: 0 0 0 0.5px
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard tiles */
|
||||||
|
|
||||||
|
.tile,.tile-user {
|
||||||
|
-fx-padding: 10px;
|
||||||
|
-fx-pref-height:200px; -fx-pref-width:200px;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
.tile .title, .tile:expanded .title,
|
||||||
|
.tile-user .title, .tile-user:expanded .title {
|
||||||
|
-fx-alignment:center-right;
|
||||||
|
-fx-font-size:1.4em;
|
||||||
|
-fx-font-weight:bold;
|
||||||
|
-fx-cursor:hand;
|
||||||
|
-fx-background-radius:2px 2px 0 0;
|
||||||
|
-fx-border-radius: 2px 2px 0 0;
|
||||||
|
-fx-border-width:1px 1px 0 1px;
|
||||||
|
-fx-background-color: rgba(255,255,255,0.5);
|
||||||
|
-fx-border-color:rgb(160,160,160); /*t r b l */
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile .title .text, .tile:expanded .title .text,
|
||||||
|
.tile-user .title .text, .tile-user:expanded .title .text {
|
||||||
|
-fx-fill:rgb(65,65,65);
|
||||||
|
}
|
||||||
|
.tile .content,
|
||||||
|
.tile-user .content {
|
||||||
|
-fx-background-color: rgba(255,255,255,0.7);
|
||||||
|
-fx-background-size:Auto 90%;
|
||||||
|
-fx-background-repeat:no-repeat;
|
||||||
|
-fx-background-position:center center;
|
||||||
|
-fx-cursor:hand;
|
||||||
|
-fx-background-radius:0 0 2px 2px;
|
||||||
|
-fx-border-radius: 0 0 2px 2px;
|
||||||
|
-fx-padding:0px;
|
||||||
|
-fx-alignment:bottom-right;
|
||||||
|
-fx-border-color:rgb(150,150,150); /*t r b l */
|
||||||
|
}
|
||||||
|
.tile .label,
|
||||||
|
.tile-user .label {
|
||||||
|
-fx-font-size:2.4em;
|
||||||
|
-fx-padding:20px;
|
||||||
|
-fx-text-fill:rgb(65,65,65);
|
||||||
|
-fx-font-weight:normal;
|
||||||
|
-fx-text-alignment:right;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile:hover .label,
|
||||||
|
.tile-user:hover .label {
|
||||||
|
-fx-padding:24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile:hover .content, .tile:hover .title,
|
||||||
|
.tile-user:hover .content, .tile-user:hover .title {
|
||||||
|
-fx-border-color:rgb(20,136,204);
|
||||||
|
-fx-background-color: rgb(20,136,204);
|
||||||
|
|
||||||
|
}
|
||||||
|
.tile:hover, .tile-user:hover {
|
||||||
|
-fx-padding:4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile:hover .label, .tile:hover .label .text, .tile:hover .title .text {
|
||||||
|
-fx-text-fill:rgb(255,255,255);
|
||||||
|
-fx-fill:rgb(255,255,255);
|
||||||
|
-fx-font-weight:bold;
|
||||||
|
-fx-effect:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tile_cash .content {
|
||||||
|
-fx-background-image:url('../images/cash_lrg.png');
|
||||||
|
}
|
||||||
|
#tile_debtors .content {
|
||||||
|
-fx-background-image:url('../images/outflow_lrg.png');
|
||||||
|
}
|
||||||
|
#tile_creditors .content {
|
||||||
|
-fx-background-image:url('../images/inflow_lrg.png');
|
||||||
|
}
|
||||||
|
#tile_tx .content {
|
||||||
|
-fx-background-image:url('../images/tx_lrg.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
#tile_cpty .content {
|
||||||
|
-fx-background-image:url('../images/cpty_lrg.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-user .content {
|
||||||
|
-fx-background-image:url('../images/user_b.png');
|
||||||
|
}
|
||||||
|
.tile-user-test-man .content {
|
||||||
|
-fx-background-image:url('../images/man1.png');
|
||||||
|
-fx-background-size:cover;
|
||||||
|
}
|
||||||
|
.tile-user-test-woman .content {
|
||||||
|
-fx-background-image:url('../images/woman1.png');
|
||||||
|
-fx-background-size:cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-user .label {
|
||||||
|
-fx-background-color:rgba(255,255,255,0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-user:hover .title, .tile-user:hover .content {
|
||||||
|
-fx-background-color:rgba(255,255,255,0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.counterparty {
|
||||||
|
-fx-background-image:url('../images/inst_128.png');
|
||||||
|
-fx-background-size:Auto 16px;
|
||||||
|
-fx-background-repeat: no-repeat;
|
||||||
|
-fx-background-position:0px center;
|
||||||
|
-fx-padding:0 0 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-panel{
|
||||||
|
-fx-background-color: rgba(255,255,255,0.7);
|
||||||
|
-fx-border-color:rgb(150,150,150);
|
||||||
|
-fx-insets:5px
|
||||||
|
}
|
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.5 KiB |
BIN
explorer/src/main/resources/com/r3corda/explorer/images/cash.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 18 KiB |
BIN
explorer/src/main/resources/com/r3corda/explorer/images/home.png
Normal file
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 7.0 KiB |
BIN
explorer/src/main/resources/com/r3corda/explorer/images/inst.png
Normal file
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 4.4 KiB |
BIN
explorer/src/main/resources/com/r3corda/explorer/images/man1.png
Normal file
After Width: | Height: | Size: 326 KiB |
After Width: | Height: | Size: 6.8 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 3.7 KiB |
BIN
explorer/src/main/resources/com/r3corda/explorer/images/tx.png
Normal file
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 12 KiB |
@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
<svg id="svg15261" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="297mm" width="210mm" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" viewBox="0 0 744.09448819 1052.3622047">
|
||||||
|
<metadata id="metadata15266">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
||||||
|
<dc:title/>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g id="layer1">
|
||||||
|
<g id="g16332" transform="matrix(22.359 0 0 22.359 -19527 -23416)" fill="#1488cc">
|
||||||
|
<g id="g45283" fill="#1488cc">
|
||||||
|
<path id="path6608" d="m891.63 1061.7c0 2.0866-1.695 3.7802-3.7844 3.7802-2.0872 0-3.7822-1.6936-3.7822-3.7802 0-2.0906 1.695-3.7826 3.7822-3.7826 2.0894 0 3.7844 1.692 3.7844 3.7826"/>
|
||||||
|
<path id="path6610" d="m894.18 1076.1c0 2.4219-1.633 2.7474-3.6493 2.7474h-5.3908c-2.0119 0-3.6448-0.3255-3.6448-2.7474l1.0547-6.3223c0-2.02 1.3627-3.6582 3.0422-3.6582h4.4913c1.6795 0 3.04 1.6382 3.04 3.6582l1.0569 6.3223"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 8.8 KiB |
After Width: | Height: | Size: 485 KiB |
@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.scene.control.Label?>
|
||||||
|
<?import javafx.scene.layout.HBox?>
|
||||||
|
<?import javafx.scene.layout.VBox?>
|
||||||
|
|
||||||
|
<HBox spacing="5.0" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||||
|
<children>
|
||||||
|
<VBox HBox.hgrow="SOMETIMES">
|
||||||
|
<children>
|
||||||
|
<Label text="State ID" VBox.vgrow="ALWAYS" />
|
||||||
|
<Label text="Issuer" />
|
||||||
|
<Label text="Originated" wrapText="true" />
|
||||||
|
<Label text="Amount" wrapText="true" />
|
||||||
|
<Label fx:id="equivLabel" text="USD" wrapText="true" />
|
||||||
|
</children>
|
||||||
|
</VBox>
|
||||||
|
<VBox HBox.hgrow="ALWAYS">
|
||||||
|
<children>
|
||||||
|
<Label fx:id="stateIdValueLabel" text="39043-329090-390091" />
|
||||||
|
<Label fx:id="issuerValueLabel" styleClass="counterparty" text="C-03820 HSBC GROUP PLC" />
|
||||||
|
<Label fx:id="originatedValueLabel" text="2018-04-27 11:34 UTC" />
|
||||||
|
<Label fx:id="amountValueLabel" text="GBP 0.00" wrapText="true" />
|
||||||
|
<Label fx:id="equivValueLabel" text="0.00" wrapText="true" />
|
||||||
|
</children>
|
||||||
|
</VBox>
|
||||||
|
</children>
|
||||||
|
</HBox>
|
@ -0,0 +1,84 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import java.lang.String?>
|
||||||
|
<?import javafx.geometry.Insets?>
|
||||||
|
<?import javafx.scene.control.Button?>
|
||||||
|
<?import javafx.scene.control.Label?>
|
||||||
|
<?import javafx.scene.control.ListView?>
|
||||||
|
<?import javafx.scene.control.SplitPane?>
|
||||||
|
<?import javafx.scene.control.TextField?>
|
||||||
|
<?import javafx.scene.control.TreeTableColumn?>
|
||||||
|
<?import javafx.scene.control.TreeTableView?>
|
||||||
|
<?import javafx.scene.image.Image?>
|
||||||
|
<?import javafx.scene.image.ImageView?>
|
||||||
|
<?import javafx.scene.layout.StackPane?>
|
||||||
|
<?import javafx.scene.layout.VBox?>
|
||||||
|
|
||||||
|
<SplitPane fx:id="topSplitPane" dividerPositions="0.5" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||||
|
<items>
|
||||||
|
<VBox fx:id="leftPane" spacing="5.0" styleClass="root">
|
||||||
|
<children>
|
||||||
|
<StackPane alignment="CENTER_RIGHT">
|
||||||
|
<VBox.margin>
|
||||||
|
<Insets />
|
||||||
|
</VBox.margin>
|
||||||
|
<children>
|
||||||
|
<TextField id="search" fx:id="searchCriteriaTextField" promptText="Search by issuer/currency" styleClass="search">
|
||||||
|
<opaqueInsets>
|
||||||
|
<Insets />
|
||||||
|
</opaqueInsets>
|
||||||
|
<StackPane.margin>
|
||||||
|
<Insets />
|
||||||
|
</StackPane.margin>
|
||||||
|
<padding>
|
||||||
|
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||||
|
</padding>
|
||||||
|
</TextField>
|
||||||
|
<ImageView fx:id="searchCancelImageView" fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true" styleClass="search-clear">
|
||||||
|
<image>
|
||||||
|
<Image url="@../../images/clear_inactive.png" />
|
||||||
|
</image>
|
||||||
|
<StackPane.margin>
|
||||||
|
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||||
|
</StackPane.margin>
|
||||||
|
</ImageView>
|
||||||
|
</children>
|
||||||
|
</StackPane>
|
||||||
|
<Label fx:id="totalMatchingLabel" alignment="BOTTOM_LEFT" text="Total 15 matching issuer(s)" wrapText="true">
|
||||||
|
<VBox.margin>
|
||||||
|
<Insets bottom="5.0" right="10.0" top="5.0" />
|
||||||
|
</VBox.margin>
|
||||||
|
</Label>
|
||||||
|
<TreeTableView fx:id="cashViewerTable" showRoot="false" VBox.vgrow="ALWAYS">
|
||||||
|
<columns>
|
||||||
|
<TreeTableColumn fx:id="cashViewerTableIssuerCurrency" maxWidth="1.7976931348623157E308" minWidth="-1.0" prefWidth="100.0" styleClass="first-column" text="Issuer/Currency" />
|
||||||
|
<TreeTableColumn fx:id="cashViewerTableLocalCurrency" maxWidth="1.7976931348623157E308" minWidth="-1.0" prefWidth="132.0" text="Local currency">
|
||||||
|
<styleClass>
|
||||||
|
<String fx:value="monetary-value" />
|
||||||
|
<String fx:value="second-column" />
|
||||||
|
</styleClass>
|
||||||
|
</TreeTableColumn>
|
||||||
|
<TreeTableColumn fx:id="cashViewerTableEquiv" maxWidth="1.7976931348623157E308" minWidth="-1.0" prefWidth="72.0" styleClass="monetary-value" text="Equiv" />
|
||||||
|
</columns>
|
||||||
|
</TreeTableView>
|
||||||
|
</children>
|
||||||
|
<padding>
|
||||||
|
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||||
|
</padding>
|
||||||
|
</VBox>
|
||||||
|
<VBox fx:id="rightPane" spacing="5.0">
|
||||||
|
<children>
|
||||||
|
<Button mnemonicParsing="false" text=">>" />
|
||||||
|
<Label fx:id="totalPositionsLabel" styleClass="subline" text="Total 18 position(s)" />
|
||||||
|
<Label fx:id="equivSumLabel" styleClass="headline" text="USD 394.6k" />
|
||||||
|
<ListView fx:id="cashStatesList" VBox.vgrow="ALWAYS" />
|
||||||
|
</children>
|
||||||
|
<opaqueInsets>
|
||||||
|
<Insets />
|
||||||
|
</opaqueInsets>
|
||||||
|
<padding>
|
||||||
|
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||||
|
</padding>
|
||||||
|
</VBox>
|
||||||
|
</items>
|
||||||
|
</SplitPane>
|
@ -0,0 +1,95 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.geometry.Insets?>
|
||||||
|
<?import javafx.scene.control.Button?>
|
||||||
|
<?import javafx.scene.control.Label?>
|
||||||
|
<?import javafx.scene.control.MenuItem?>
|
||||||
|
<?import javafx.scene.control.SplitMenuButton?>
|
||||||
|
<?import javafx.scene.control.TextField?>
|
||||||
|
<?import javafx.scene.image.Image?>
|
||||||
|
<?import javafx.scene.image.ImageView?>
|
||||||
|
<?import javafx.scene.layout.HBox?>
|
||||||
|
<?import javafx.scene.layout.StackPane?>
|
||||||
|
<?import javafx.scene.layout.VBox?>
|
||||||
|
|
||||||
|
<VBox xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||||
|
<children>
|
||||||
|
<VBox spacing="5.0">
|
||||||
|
<children>
|
||||||
|
<HBox>
|
||||||
|
<children>
|
||||||
|
<HBox alignment="CENTER_LEFT" spacing="5.0" HBox.hgrow="ALWAYS">
|
||||||
|
<children>
|
||||||
|
<VBox fx:id="sectionIconContainer" alignment="CENTER">
|
||||||
|
<children>
|
||||||
|
<ImageView fx:id="sectionIcon" fitHeight="30.0" fitWidth="30.0" pickOnBounds="true" preserveRatio="true">
|
||||||
|
<image>
|
||||||
|
<Image url="@../../images/home.png" />
|
||||||
|
</image>
|
||||||
|
</ImageView>
|
||||||
|
</children>
|
||||||
|
</VBox>
|
||||||
|
<Label id="headline" fx:id="sectionLabel" text="Home" />
|
||||||
|
</children>
|
||||||
|
<padding>
|
||||||
|
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||||
|
</padding>
|
||||||
|
</HBox>
|
||||||
|
<HBox alignment="TOP_RIGHT" spacing="5.0">
|
||||||
|
<children>
|
||||||
|
<Button fx:id="debugNextButton" mnemonicParsing="false" text="Next" />
|
||||||
|
<Button fx:id="debugGoStopButton" mnemonicParsing="false" text="!!!" />
|
||||||
|
<SplitMenuButton mnemonicParsing="false" text="DRUTTER">
|
||||||
|
<items>
|
||||||
|
<MenuItem mnemonicParsing="false" text="Sign out" />
|
||||||
|
<MenuItem mnemonicParsing="false" text="Account settings..." />
|
||||||
|
</items>
|
||||||
|
<graphic>
|
||||||
|
<ImageView fitHeight="20.0" fitWidth="52.0" pickOnBounds="true" preserveRatio="true">
|
||||||
|
<image>
|
||||||
|
<Image url="@../../images/user_w.png" />
|
||||||
|
</image>
|
||||||
|
</ImageView>
|
||||||
|
</graphic>
|
||||||
|
</SplitMenuButton>
|
||||||
|
<Button fx:id="settingsButton" mnemonicParsing="false" text="Settings">
|
||||||
|
<graphic>
|
||||||
|
<ImageView fitHeight="20.0" fitWidth="20.0" pickOnBounds="true" preserveRatio="true">
|
||||||
|
<image>
|
||||||
|
<Image url="@../../images/settings_w.png" />
|
||||||
|
</image>
|
||||||
|
</ImageView>
|
||||||
|
</graphic>
|
||||||
|
</Button>
|
||||||
|
</children>
|
||||||
|
</HBox>
|
||||||
|
</children>
|
||||||
|
</HBox>
|
||||||
|
<StackPane alignment="CENTER_RIGHT">
|
||||||
|
<children>
|
||||||
|
<TextField id="search_main" promptText="Search for states, transactions, counterparties etc." styleClass="search">
|
||||||
|
<opaqueInsets>
|
||||||
|
<Insets />
|
||||||
|
</opaqueInsets>
|
||||||
|
<padding>
|
||||||
|
<Insets bottom="5.0" left="30.0" right="5.0" top="5.0" />
|
||||||
|
</padding>
|
||||||
|
</TextField>
|
||||||
|
<ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true" styleClass="search-clear">
|
||||||
|
<image>
|
||||||
|
<Image url="@../../images/clear_inactive.png" />
|
||||||
|
</image>
|
||||||
|
<StackPane.margin>
|
||||||
|
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||||
|
</StackPane.margin>
|
||||||
|
</ImageView>
|
||||||
|
</children>
|
||||||
|
</StackPane>
|
||||||
|
</children>
|
||||||
|
</VBox>
|
||||||
|
<StackPane alignment="CENTER_RIGHT" />
|
||||||
|
</children>
|
||||||
|
<padding>
|
||||||
|
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||||
|
</padding>
|
||||||
|
</VBox>
|
@ -0,0 +1,30 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.scene.control.Label?>
|
||||||
|
<?import javafx.scene.control.TitledPane?>
|
||||||
|
<?import javafx.scene.layout.TilePane?>
|
||||||
|
|
||||||
|
<TilePane prefHeight="425.0" prefWidth="425.0" tileAlignment="TOP_LEFT" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||||
|
<children>
|
||||||
|
<TitledPane id="tile_cash" fx:id="ourCashPane" alignment="CENTER" collapsible="false" prefHeight="160.0" prefWidth="160.0" styleClass="tile" text="Our cash">
|
||||||
|
<content>
|
||||||
|
<Label fx:id="ourCashLabel" text="USD 186.7m" textAlignment="CENTER" wrapText="true" />
|
||||||
|
</content>
|
||||||
|
</TitledPane>
|
||||||
|
<TitledPane id="tile_debtors" fx:id="ourDebtorsPane" alignment="CENTER" collapsible="false" layoutX="232.0" layoutY="10.0" prefHeight="160.0" prefWidth="160.0" styleClass="tile" text="Our debtors">
|
||||||
|
<content>
|
||||||
|
<Label text="USD 71.3m" textAlignment="CENTER" wrapText="true" />
|
||||||
|
</content>
|
||||||
|
</TitledPane>
|
||||||
|
<TitledPane id="tile_creditors" fx:id="ourCreditorsPane" alignment="CENTER" collapsible="false" layoutX="312.0" layoutY="10.0" prefHeight="160.0" prefWidth="160.0" styleClass="tile" text="Our creditors">
|
||||||
|
<content>
|
||||||
|
<Label text="USD (29.4m)" textAlignment="CENTER" wrapText="true" />
|
||||||
|
</content>
|
||||||
|
</TitledPane>
|
||||||
|
<TitledPane id="tile_tx" fx:id="ourTransactionsPane" alignment="CENTER" collapsible="false" layoutX="392.0" layoutY="10.0" prefHeight="160.0" prefWidth="160.0" styleClass="tile" text="Our transactions">
|
||||||
|
<content>
|
||||||
|
<Label fx:id="ourTransactionsLabel" text="In flight: 1,315" textAlignment="CENTER" wrapText="true" />
|
||||||
|
</content>
|
||||||
|
</TitledPane>
|
||||||
|
</children>
|
||||||
|
</TilePane>
|
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.scene.layout.BorderPane?>
|
||||||
|
<?import javafx.scene.layout.VBox?>
|
||||||
|
|
||||||
|
<VBox fx:id="topLevel" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||||
|
<children>
|
||||||
|
<BorderPane fx:id="selectionBorderPane" />
|
||||||
|
</children>
|
||||||
|
</VBox>
|
@ -0,0 +1,147 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import java.lang.String?>
|
||||||
|
<?import javafx.geometry.Insets?>
|
||||||
|
<?import javafx.scene.control.Label?>
|
||||||
|
<?import javafx.scene.control.ListView?>
|
||||||
|
<?import javafx.scene.control.SplitPane?>
|
||||||
|
<?import javafx.scene.control.TableColumn?>
|
||||||
|
<?import javafx.scene.control.TableView?>
|
||||||
|
<?import javafx.scene.control.TextField?>
|
||||||
|
<?import javafx.scene.control.TitledPane?>
|
||||||
|
<?import javafx.scene.image.Image?>
|
||||||
|
<?import javafx.scene.image.ImageView?>
|
||||||
|
<?import javafx.scene.layout.HBox?>
|
||||||
|
<?import javafx.scene.layout.StackPane?>
|
||||||
|
<?import javafx.scene.layout.VBox?>
|
||||||
|
|
||||||
|
<VBox styleClass="view" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||||
|
<children>
|
||||||
|
<StackPane alignment="CENTER_RIGHT">
|
||||||
|
<children>
|
||||||
|
<TextField promptText="Filter transactions by originator, contract type..." styleClass="search">
|
||||||
|
<opaqueInsets>
|
||||||
|
<Insets />
|
||||||
|
</opaqueInsets>
|
||||||
|
<padding>
|
||||||
|
<Insets bottom="5.0" left="30.0" right="5.0" top="5.0" />
|
||||||
|
</padding>
|
||||||
|
<StackPane.margin>
|
||||||
|
<Insets bottom="5.0" top="5.0" />
|
||||||
|
</StackPane.margin>
|
||||||
|
</TextField>
|
||||||
|
<ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true" styleClass="search-clear">
|
||||||
|
<image>
|
||||||
|
<Image url="@../../../../../../../../../internal/explorer/src/main/resources/images/clear_inactive.png" />
|
||||||
|
</image>
|
||||||
|
<StackPane.margin>
|
||||||
|
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||||
|
</StackPane.margin>
|
||||||
|
</ImageView>
|
||||||
|
</children>
|
||||||
|
</StackPane>
|
||||||
|
<SplitPane fx:id="topSplitPane" dividerPositions="0.3, 0.6, 0.7" orientation="VERTICAL" prefHeight="562.0" prefWidth="1087.0" VBox.vgrow="ALWAYS">
|
||||||
|
<items>
|
||||||
|
<TableView fx:id="transactionViewTable" prefHeight="200.0" prefWidth="200.0">
|
||||||
|
<columns>
|
||||||
|
<TableColumn fx:id="transactionViewTransactionId" prefWidth="75.0" text="Transaction ID" />
|
||||||
|
<TableColumn fx:id="transactionViewFiberId" prefWidth="187.0" text="Fiber ID" />
|
||||||
|
<TableColumn fx:id="transactionViewClientUuid" prefWidth="75.0" text="Client UUID" />
|
||||||
|
<TableColumn fx:id="transactionViewTransactionStatus" prefWidth="75.0" text="Transaction status" />
|
||||||
|
<TableColumn fx:id="transactionViewProtocolStatus" prefWidth="75.0" text="Protocol status" />
|
||||||
|
<TableColumn fx:id="transactionViewStateMachineStatus" prefWidth="75.0" text="SM Status" />
|
||||||
|
<TableColumn fx:id="transactionViewCommandTypes" prefWidth="75.0" text="Command type(s)" />
|
||||||
|
<TableColumn fx:id="transactionViewTotalValueEquiv" prefWidth="75.0" styleClass="monetary-value" text="Total value (USD equiv)" />
|
||||||
|
</columns>
|
||||||
|
<columnResizePolicy>
|
||||||
|
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
|
||||||
|
</columnResizePolicy>
|
||||||
|
</TableView>
|
||||||
|
<TitledPane fx:id="contractStatesTitledPane" animated="false" text="Contract states">
|
||||||
|
<content>
|
||||||
|
<SplitPane dividerPositions="0.5" prefHeight="160.0" prefWidth="200.0">
|
||||||
|
<items>
|
||||||
|
<VBox prefHeight="200.0" prefWidth="100.0">
|
||||||
|
<children>
|
||||||
|
<HBox spacing="5.0">
|
||||||
|
<children>
|
||||||
|
<Label text="Inputs:" />
|
||||||
|
<Label fx:id="contractStatesInputsCountLabel" text="Label" />
|
||||||
|
</children>
|
||||||
|
</HBox>
|
||||||
|
<TableView fx:id="contractStatesInputStatesTable" prefHeight="200.0" prefWidth="200.0" VBox.vgrow="ALWAYS">
|
||||||
|
<columns>
|
||||||
|
<TableColumn fx:id="contractStatesInputStatesId" prefWidth="75.0" text="ID" />
|
||||||
|
<TableColumn fx:id="contractStatesInputStatesType" prefWidth="75.0" text="Type" />
|
||||||
|
<TableColumn fx:id="contractStatesInputStatesOwner" prefWidth="75.0" text="Owner" />
|
||||||
|
<TableColumn fx:id="contractStatesInputStatesLocalCurrency" prefWidth="75.0" styleClass="first-column" text="Local Ccy" />
|
||||||
|
<TableColumn fx:id="contractStatesInputStatesAmount" prefWidth="75.0" text="Amount">
|
||||||
|
<styleClass>
|
||||||
|
<String fx:value="second-column" />
|
||||||
|
<String fx:value="monetary-value" />
|
||||||
|
</styleClass>
|
||||||
|
</TableColumn>
|
||||||
|
<TableColumn fx:id="contractStatesInputStatesEquiv" prefWidth="75.0" text="USD Equiv" />
|
||||||
|
</columns>
|
||||||
|
<columnResizePolicy>
|
||||||
|
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
|
||||||
|
</columnResizePolicy>
|
||||||
|
</TableView>
|
||||||
|
</children>
|
||||||
|
</VBox>
|
||||||
|
<VBox prefHeight="200.0" prefWidth="100.0">
|
||||||
|
<children>
|
||||||
|
<HBox spacing="5.0">
|
||||||
|
<children>
|
||||||
|
<Label text="Outputs:" />
|
||||||
|
<Label fx:id="contractStatesOutputsCountLabel" text="Label" />
|
||||||
|
</children>
|
||||||
|
</HBox>
|
||||||
|
<TableView fx:id="contractStatesOutputStatesTable" prefHeight="200.0" prefWidth="200.0" VBox.vgrow="ALWAYS">
|
||||||
|
<columns>
|
||||||
|
<TableColumn fx:id="contractStatesOutputStatesId" prefWidth="75.0" text="ID" />
|
||||||
|
<TableColumn fx:id="contractStatesOutputStatesType" prefWidth="75.0" text="Type" />
|
||||||
|
<TableColumn fx:id="contractStatesOutputStatesOwner" prefWidth="75.0" text="Owner" />
|
||||||
|
<TableColumn fx:id="contractStatesOutputStatesLocalCurrency" prefWidth="75.0" styleClass="first-column" text="Local Ccy" />
|
||||||
|
<TableColumn fx:id="contractStatesOutputStatesAmount" prefWidth="75.0" text="Amount">
|
||||||
|
<styleClass>
|
||||||
|
<String fx:value="second-column" />
|
||||||
|
<String fx:value="monetary-value" />
|
||||||
|
</styleClass>
|
||||||
|
</TableColumn>
|
||||||
|
<TableColumn fx:id="contractStatesOutputStatesEquiv" prefWidth="75.0" text="USD Equiv" />
|
||||||
|
</columns>
|
||||||
|
<columnResizePolicy>
|
||||||
|
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
|
||||||
|
</columnResizePolicy>
|
||||||
|
</TableView>
|
||||||
|
</children>
|
||||||
|
</VBox>
|
||||||
|
</items>
|
||||||
|
</SplitPane>
|
||||||
|
</content>
|
||||||
|
</TitledPane>
|
||||||
|
<TitledPane fx:id="signaturesTitledPane" animated="false" text="Required signatures">
|
||||||
|
<content>
|
||||||
|
<ListView fx:id="signaturesList" />
|
||||||
|
</content>
|
||||||
|
</TitledPane>
|
||||||
|
<TitledPane fx:id="lowLevelEventsTitledPane" animated="false" text="Low level events">
|
||||||
|
<content>
|
||||||
|
<TableView fx:id="lowLevelEventsTable">
|
||||||
|
<columns>
|
||||||
|
<TableColumn fx:id="lowLevelEventsTimestamp" prefWidth="102.0" text="Timestamp" />
|
||||||
|
<TableColumn fx:id="lowLevelEventsEvent" prefWidth="138.0" text="Event" />
|
||||||
|
</columns>
|
||||||
|
</TableView>
|
||||||
|
</content>
|
||||||
|
</TitledPane>
|
||||||
|
</items>
|
||||||
|
</SplitPane>
|
||||||
|
<HBox>
|
||||||
|
<children>
|
||||||
|
<Label fx:id="matchingTransactionsLabel" text="matching transaction(s)" />
|
||||||
|
</children>
|
||||||
|
</HBox>
|
||||||
|
</children>
|
||||||
|
</VBox>
|
@ -0,0 +1,65 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import java.lang.String?>
|
||||||
|
<?import javafx.geometry.Insets?>
|
||||||
|
<?import javafx.scene.control.ComboBox?>
|
||||||
|
<?import javafx.scene.control.DialogPane?>
|
||||||
|
<?import javafx.scene.control.Label?>
|
||||||
|
<?import javafx.scene.control.PasswordField?>
|
||||||
|
<?import javafx.scene.control.TextField?>
|
||||||
|
<?import javafx.scene.control.TitledPane?>
|
||||||
|
<?import javafx.scene.layout.TilePane?>
|
||||||
|
<?import javafx.scene.layout.VBox?>
|
||||||
|
|
||||||
|
<DialogPane expanded="true" headerText="Sign in to Corda" prefHeight="246.0" prefWidth="648.0" xmlns="http://javafx.com/javafx/8.0.60" xmlns:fx="http://javafx.com/fxml/1">
|
||||||
|
<content>
|
||||||
|
<TilePane alignment="CENTER">
|
||||||
|
<children>
|
||||||
|
<TitledPane alignment="CENTER" collapsible="false" prefHeight="160.0" prefWidth="160.0" text="Log in">
|
||||||
|
<content>
|
||||||
|
<Label alignment="BOTTOM_RIGHT" prefHeight="200.0" prefWidth="200.0" text="Thomas" textAlignment="CENTER" wrapText="true" />
|
||||||
|
</content>
|
||||||
|
<styleClass>
|
||||||
|
<String fx:value="tile-user" />
|
||||||
|
<String fx:value="tile-user-test-man" />
|
||||||
|
</styleClass>
|
||||||
|
</TitledPane>
|
||||||
|
<TitledPane alignment="CENTER" collapsible="false" layoutX="232.0" layoutY="10.0" prefHeight="160.0" prefWidth="160.0" text="Log in">
|
||||||
|
<styleClass>
|
||||||
|
<String fx:value="tile-user" />
|
||||||
|
<String fx:value="tile-user-test-woman" />
|
||||||
|
</styleClass>
|
||||||
|
<content>
|
||||||
|
<Label alignment="BOTTOM_RIGHT" prefHeight="200.0" prefWidth="200.0" text="Theresa" textAlignment="CENTER" wrapText="true" />
|
||||||
|
</content>
|
||||||
|
</TitledPane>
|
||||||
|
<TitledPane alignment="CENTER" collapsible="false" layoutX="312.0" layoutY="10.0" prefHeight="160.0" prefWidth="160.0" styleClass="tile-user" text="Log in">
|
||||||
|
<content>
|
||||||
|
<Label alignment="BOTTOM_RIGHT" prefHeight="200.0" prefWidth="200.0" text="Other user" textAlignment="CENTER" wrapText="true" />
|
||||||
|
</content>
|
||||||
|
</TitledPane>
|
||||||
|
</children>
|
||||||
|
</TilePane>
|
||||||
|
</content>
|
||||||
|
<expandableContent>
|
||||||
|
<VBox alignment="TOP_CENTER">
|
||||||
|
<children>
|
||||||
|
<ComboBox editable="true" maxWidth="300.0" prefWidth="300.0" promptText="Server name">
|
||||||
|
<VBox.margin>
|
||||||
|
<Insets top="5.0" />
|
||||||
|
</VBox.margin>
|
||||||
|
</ComboBox>
|
||||||
|
<TextField maxWidth="300.0" promptText="User name">
|
||||||
|
<VBox.margin>
|
||||||
|
<Insets top="5.0" />
|
||||||
|
</VBox.margin>
|
||||||
|
</TextField>
|
||||||
|
<PasswordField maxWidth="300.0" prefWidth="300.0" promptText="Password">
|
||||||
|
<VBox.margin>
|
||||||
|
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||||
|
</VBox.margin>
|
||||||
|
</PasswordField>
|
||||||
|
</children>
|
||||||
|
</VBox>
|
||||||
|
</expandableContent>
|
||||||
|
</DialogPane>
|
@ -0,0 +1,75 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import java.lang.String?>
|
||||||
|
<?import javafx.geometry.Insets?>
|
||||||
|
<?import javafx.scene.control.Label?>
|
||||||
|
<?import javafx.scene.control.TextField?>
|
||||||
|
<?import javafx.scene.control.TreeTableColumn?>
|
||||||
|
<?import javafx.scene.control.TreeTableView?>
|
||||||
|
<?import javafx.scene.image.Image?>
|
||||||
|
<?import javafx.scene.image.ImageView?>
|
||||||
|
<?import javafx.scene.layout.StackPane?>
|
||||||
|
<?import javafx.scene.layout.VBox?>
|
||||||
|
|
||||||
|
<VBox xmlns="http://javafx.com/javafx/8.0.60" xmlns:fx="http://javafx.com/fxml/1">
|
||||||
|
<children>
|
||||||
|
<StackPane alignment="CENTER_RIGHT">
|
||||||
|
<VBox.margin>
|
||||||
|
<Insets bottom="5.0" />
|
||||||
|
</VBox.margin>
|
||||||
|
<children>
|
||||||
|
<TextField promptText="Search by creditor" styleClass="search">
|
||||||
|
<opaqueInsets>
|
||||||
|
<Insets />
|
||||||
|
</opaqueInsets>
|
||||||
|
<StackPane.margin>
|
||||||
|
<Insets />
|
||||||
|
</StackPane.margin>
|
||||||
|
<padding>
|
||||||
|
<Insets bottom="5.0" left="30.0" right="5.0" top="5.0" />
|
||||||
|
</padding>
|
||||||
|
</TextField>
|
||||||
|
<ImageView id="clear" fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true" styleClass="search-clear">
|
||||||
|
<image>
|
||||||
|
<Image url="@Desktop/clear_inactive.png" />
|
||||||
|
</image>
|
||||||
|
<StackPane.margin>
|
||||||
|
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||||
|
</StackPane.margin>
|
||||||
|
</ImageView>
|
||||||
|
</children>
|
||||||
|
</StackPane>
|
||||||
|
<VBox />
|
||||||
|
<TreeTableView showRoot="false" sortMode="ONLY_FIRST_LEVEL" VBox.vgrow="ALWAYS">
|
||||||
|
<columns>
|
||||||
|
<TreeTableColumn prefWidth="240.0" styleClass="first-column" text="Creditor" />
|
||||||
|
<TreeTableColumn editable="false" minWidth="0.0" prefWidth="122.0" text="Total outstanding">
|
||||||
|
<styleClass>
|
||||||
|
<String fx:value="montetary-value" />
|
||||||
|
<String fx:value="second-column" />
|
||||||
|
</styleClass></TreeTableColumn>
|
||||||
|
<TreeTableColumn editable="false" minWidth="0.0" prefWidth="156.0" styleClass="montetary-value" text="Overdue" />
|
||||||
|
<TreeTableColumn prefWidth="75.0" styleClass="montetary-value" text="1d" />
|
||||||
|
<TreeTableColumn prefWidth="75.0" styleClass="montetary-value" text="2-7d" />
|
||||||
|
<TreeTableColumn prefWidth="75.0" styleClass="montetary-value" text="8-14d" />
|
||||||
|
<TreeTableColumn prefWidth="75.0" styleClass="montetary-value" text="14d-1m" />
|
||||||
|
<TreeTableColumn prefWidth="75.0" styleClass="montetary-value" text="1-3m" />
|
||||||
|
<TreeTableColumn prefWidth="75.0" styleClass="montetary-value" text="3-6m" />
|
||||||
|
<TreeTableColumn prefWidth="75.0" styleClass="montetary-value" text="6m-1yr" />
|
||||||
|
<TreeTableColumn prefWidth="75.0" styleClass="montetary-value" text="1-5yr" />
|
||||||
|
<TreeTableColumn prefWidth="75.0" styleClass="montetary-value" text=">5yr" />
|
||||||
|
</columns>
|
||||||
|
<columnResizePolicy>
|
||||||
|
<TreeTableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
|
||||||
|
</columnResizePolicy>
|
||||||
|
</TreeTableView>
|
||||||
|
<Label alignment="BOTTOM_LEFT" prefHeight="16.0" text="Total 15 matching issuer(s)" wrapText="true">
|
||||||
|
<VBox.margin>
|
||||||
|
<Insets bottom="5.0" right="10.0" top="5.0" />
|
||||||
|
</VBox.margin>
|
||||||
|
</Label>
|
||||||
|
</children>
|
||||||
|
<padding>
|
||||||
|
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
|
||||||
|
</padding>
|
||||||
|
</VBox>
|
@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.geometry.Insets?>
|
||||||
|
<?import javafx.scene.control.TextField?>
|
||||||
|
<?import javafx.scene.image.Image?>
|
||||||
|
<?import javafx.scene.image.ImageView?>
|
||||||
|
<?import javafx.scene.layout.StackPane?>
|
||||||
|
|
||||||
|
<StackPane alignment="CENTER_RIGHT" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||||
|
<children>
|
||||||
|
<TextField fx:id="SearchCriteriaTextField" promptText="Set prompt text" styleClass="search">
|
||||||
|
<opaqueInsets>
|
||||||
|
<Insets />
|
||||||
|
</opaqueInsets>
|
||||||
|
<StackPane.margin>
|
||||||
|
<Insets />
|
||||||
|
</StackPane.margin>
|
||||||
|
</TextField>
|
||||||
|
<ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true" styleClass="search-clear" StackPane.alignment="CENTER_RIGHT">
|
||||||
|
<image>
|
||||||
|
<Image url="@../../images/clear_inactive.png" />
|
||||||
|
</image>
|
||||||
|
<StackPane.margin>
|
||||||
|
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||||
|
</StackPane.margin>
|
||||||
|
</ImageView>
|
||||||
|
</children>
|
||||||
|
</StackPane>
|
@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.geometry.Insets?>
|
||||||
|
<?import javafx.scene.control.TextField?>
|
||||||
|
<?import javafx.scene.image.Image?>
|
||||||
|
<?import javafx.scene.image.ImageView?>
|
||||||
|
<?import javafx.scene.layout.StackPane?>
|
||||||
|
|
||||||
|
<StackPane alignment="CENTER_RIGHT" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||||
|
<children>
|
||||||
|
<TextField fx:id="SearchCriteriaTextField" promptText="Set prompt text" styleClass="search">
|
||||||
|
<opaqueInsets>
|
||||||
|
<Insets />
|
||||||
|
</opaqueInsets>
|
||||||
|
<StackPane.margin>
|
||||||
|
<Insets />
|
||||||
|
</StackPane.margin>
|
||||||
|
</TextField>
|
||||||
|
<ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true" styleClass="search-clear" StackPane.alignment="CENTER_RIGHT">
|
||||||
|
<image>
|
||||||
|
<Image url="@../../images/clear_inactive.png" />
|
||||||
|
</image>
|
||||||
|
<StackPane.margin>
|
||||||
|
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||||
|
</StackPane.margin>
|
||||||
|
</ImageView>
|
||||||
|
</children>
|
||||||
|
</StackPane>
|
@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.scene.control.ButtonType?>
|
||||||
|
<?import javafx.scene.control.ChoiceBox?>
|
||||||
|
<?import javafx.scene.control.DialogPane?>
|
||||||
|
<?import javafx.scene.control.Label?>
|
||||||
|
<?import javafx.scene.layout.AnchorPane?>
|
||||||
|
|
||||||
|
|
||||||
|
<DialogPane expanded="true" headerText="Settings" scaleShape="false" xmlns="http://javafx.com/javafx/8.0.60" xmlns:fx="http://javafx.com/fxml/1">
|
||||||
|
<content>
|
||||||
|
<AnchorPane>
|
||||||
|
<children>
|
||||||
|
<Label layoutX="6.0" layoutY="6.0" text="We are" />
|
||||||
|
<ChoiceBox layoutX="146.0" layoutY="2.0" prefHeight="24.0" prefWidth="360.0" AnchorPane.leftAnchor="146.0" />
|
||||||
|
<Label layoutX="6.0" layoutY="37.0" text="Reporting currency" />
|
||||||
|
<ChoiceBox layoutX="156.0" layoutY="33.0" prefWidth="150.0" />
|
||||||
|
</children>
|
||||||
|
</AnchorPane>
|
||||||
|
</content>
|
||||||
|
<buttonTypes>
|
||||||
|
<ButtonType fx:constant="APPLY" />
|
||||||
|
<ButtonType fx:constant="CLOSE" />
|
||||||
|
</buttonTypes>
|
||||||
|
</DialogPane>
|
@ -0,0 +1,47 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import java.lang.String?>
|
||||||
|
<?import javafx.geometry.Insets?>
|
||||||
|
<?import javafx.scene.control.ButtonType?>
|
||||||
|
<?import javafx.scene.control.DialogPane?>
|
||||||
|
<?import javafx.scene.control.Label?>
|
||||||
|
<?import javafx.scene.control.TableColumn?>
|
||||||
|
<?import javafx.scene.control.TableView?>
|
||||||
|
<?import javafx.scene.layout.VBox?>
|
||||||
|
|
||||||
|
|
||||||
|
<DialogPane headerText="Issuer C039201 HSBC GROUP PLC" xmlns="http://javafx.com/javafx/8.0.60" xmlns:fx="http://javafx.com/fxml/1">
|
||||||
|
<content>
|
||||||
|
<VBox spacing="5.0">
|
||||||
|
<children>
|
||||||
|
<Label styleClass="subline" text="Total 18 position(s)" />
|
||||||
|
<Label styleClass="headline" text="USD 394.6k" />
|
||||||
|
<TableView prefHeight="284.0" prefWidth="417.0">
|
||||||
|
<columns>
|
||||||
|
<TableColumn prefWidth="155.0" text="Position ID" />
|
||||||
|
<TableColumn prefWidth="123.0" styleClass="first-column" text="Local ccy" />
|
||||||
|
<TableColumn prefWidth="79.0" text="Amount">
|
||||||
|
<styleClass>
|
||||||
|
<String fx:value="second-column" />
|
||||||
|
<String fx:value="monetary-value" />
|
||||||
|
</styleClass>
|
||||||
|
</TableColumn>
|
||||||
|
<TableColumn prefWidth="75.0" styleClass="monetary-value" text="USD equiv" />
|
||||||
|
</columns>
|
||||||
|
<columnResizePolicy>
|
||||||
|
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
|
||||||
|
</columnResizePolicy>
|
||||||
|
</TableView>
|
||||||
|
</children>
|
||||||
|
<opaqueInsets>
|
||||||
|
<Insets />
|
||||||
|
</opaqueInsets>
|
||||||
|
<padding>
|
||||||
|
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||||
|
</padding>
|
||||||
|
</VBox>
|
||||||
|
</content>
|
||||||
|
<buttonTypes>
|
||||||
|
<ButtonType fx:constant="CLOSE" />
|
||||||
|
</buttonTypes>
|
||||||
|
</DialogPane>
|
@ -1,7 +1,7 @@
|
|||||||
package com.r3corda.node.services.monitor
|
package com.r3corda.node.services.monitor
|
||||||
|
|
||||||
import com.r3corda.core.contracts.*
|
import com.r3corda.core.contracts.*
|
||||||
import com.r3corda.core.transactions.SignedTransaction
|
import com.r3corda.core.transactions.LedgerTransaction
|
||||||
import com.r3corda.node.utilities.AddOrRemove
|
import com.r3corda.node.utilities.AddOrRemove
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@ -10,8 +10,8 @@ import java.util.*
|
|||||||
* Events triggered by changes in the node, and sent to monitoring client(s).
|
* Events triggered by changes in the node, and sent to monitoring client(s).
|
||||||
*/
|
*/
|
||||||
sealed class ServiceToClientEvent(val time: Instant) {
|
sealed class ServiceToClientEvent(val time: Instant) {
|
||||||
class Transaction(time: Instant, val transaction: SignedTransaction) : ServiceToClientEvent(time) {
|
class Transaction(time: Instant, val transaction: LedgerTransaction) : ServiceToClientEvent(time) {
|
||||||
override fun toString() = "Transaction(${transaction.tx.commands})"
|
override fun toString() = "Transaction(${transaction.commands})"
|
||||||
}
|
}
|
||||||
class OutputState(
|
class OutputState(
|
||||||
time: Instant,
|
time: Instant,
|
||||||
@ -26,7 +26,7 @@ sealed class ServiceToClientEvent(val time: Instant) {
|
|||||||
val label: String,
|
val label: String,
|
||||||
val addOrRemove: AddOrRemove
|
val addOrRemove: AddOrRemove
|
||||||
) : ServiceToClientEvent(time) {
|
) : ServiceToClientEvent(time) {
|
||||||
override fun toString() = "StateMachine(${addOrRemove.name})"
|
override fun toString() = "StateMachine($label, ${addOrRemove.name})"
|
||||||
}
|
}
|
||||||
class Progress(time: Instant, val fiberId: Long, val message: String) : ServiceToClientEvent(time) {
|
class Progress(time: Instant, val fiberId: Long, val message: String) : ServiceToClientEvent(time) {
|
||||||
override fun toString() = "Progress($message)"
|
override fun toString() = "Progress($message)"
|
||||||
@ -46,7 +46,7 @@ sealed class TransactionBuildResult {
|
|||||||
*
|
*
|
||||||
* @param transaction the transaction created as a result, in the case where the protocol has completed.
|
* @param transaction the transaction created as a result, in the case where the protocol has completed.
|
||||||
*/
|
*/
|
||||||
class ProtocolStarted(val fiberId: Long, val transaction: SignedTransaction?, val message: String?) : TransactionBuildResult() {
|
class ProtocolStarted(val fiberId: Long, val transaction: LedgerTransaction?, val message: String?) : TransactionBuildResult() {
|
||||||
override fun toString() = "Started($message)"
|
override fun toString() = "Started($message)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ package com.r3corda.node.services.monitor
|
|||||||
|
|
||||||
import com.r3corda.core.contracts.ClientToServiceCommand
|
import com.r3corda.core.contracts.ClientToServiceCommand
|
||||||
import com.r3corda.core.contracts.ContractState
|
import com.r3corda.core.contracts.ContractState
|
||||||
|
import com.r3corda.core.contracts.StateAndRef
|
||||||
import com.r3corda.core.messaging.SingleMessageRecipient
|
import com.r3corda.core.messaging.SingleMessageRecipient
|
||||||
import com.r3corda.protocols.DirectRequestMessage
|
import com.r3corda.protocols.DirectRequestMessage
|
||||||
|
|
||||||
@ -14,6 +15,6 @@ data class DeregisterRequest(override val replyToRecipient: SingleMessageRecipie
|
|||||||
override val sessionID: Long) : DirectRequestMessage
|
override val sessionID: Long) : DirectRequestMessage
|
||||||
|
|
||||||
data class DeregisterResponse(val success: Boolean)
|
data class DeregisterResponse(val success: Boolean)
|
||||||
data class StateSnapshotMessage(val contractStates: Collection<ContractState>, val protocolStates: Collection<String>)
|
data class StateSnapshotMessage(val contractStates: Collection<StateAndRef<ContractState>>, val protocolStates: Collection<String>)
|
||||||
|
|
||||||
data class ClientToServiceCommandMessage(override val sessionID: Long, override val replyToRecipient: SingleMessageRecipient, val command: ClientToServiceCommand) : DirectRequestMessage
|
data class ClientToServiceCommandMessage(override val sessionID: Long, override val replyToRecipient: SingleMessageRecipient, val command: ClientToServiceCommand) : DirectRequestMessage
|
||||||
|
@ -11,7 +11,7 @@ import com.r3corda.core.node.services.DEFAULT_SESSION_ID
|
|||||||
import com.r3corda.core.node.services.Wallet
|
import com.r3corda.core.node.services.Wallet
|
||||||
import com.r3corda.core.protocols.ProtocolLogic
|
import com.r3corda.core.protocols.ProtocolLogic
|
||||||
import com.r3corda.core.serialization.serialize
|
import com.r3corda.core.serialization.serialize
|
||||||
import com.r3corda.core.transactions.SignedTransaction
|
import com.r3corda.core.transactions.LedgerTransaction
|
||||||
import com.r3corda.core.transactions.TransactionBuilder
|
import com.r3corda.core.transactions.TransactionBuilder
|
||||||
import com.r3corda.core.utilities.loggerFor
|
import com.r3corda.core.utilities.loggerFor
|
||||||
import com.r3corda.node.services.api.AbstractNodeService
|
import com.r3corda.node.services.api.AbstractNodeService
|
||||||
@ -59,7 +59,7 @@ class WalletMonitorService(services: ServiceHubInternal, val smm: StateMachineMa
|
|||||||
addMessageHandler(OUT_EVENT_TOPIC) { req: ClientToServiceCommandMessage -> processEventRequest(req) }
|
addMessageHandler(OUT_EVENT_TOPIC) { req: ClientToServiceCommandMessage -> processEventRequest(req) }
|
||||||
|
|
||||||
// Notify listeners on state changes
|
// Notify listeners on state changes
|
||||||
services.storageService.validatedTransactions.updates.subscribe { tx -> notifyTransaction(tx) }
|
services.storageService.validatedTransactions.updates.subscribe { tx -> notifyTransaction(tx.tx.toLedgerTransaction(services)) }
|
||||||
services.walletService.updates.subscribe { update -> notifyWalletUpdate(update) }
|
services.walletService.updates.subscribe { update -> notifyWalletUpdate(update) }
|
||||||
smm.changes.subscribe { change ->
|
smm.changes.subscribe { change ->
|
||||||
val fiberId: Long = change.third
|
val fiberId: Long = change.third
|
||||||
@ -85,7 +85,7 @@ class WalletMonitorService(services: ServiceHubInternal, val smm: StateMachineMa
|
|||||||
= notifyEvent(ServiceToClientEvent.OutputState(Instant.now(), update.consumed, update.produced))
|
= notifyEvent(ServiceToClientEvent.OutputState(Instant.now(), update.consumed, update.produced))
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
internal fun notifyTransaction(transaction: SignedTransaction)
|
internal fun notifyTransaction(transaction: LedgerTransaction)
|
||||||
= notifyEvent(ServiceToClientEvent.Transaction(Instant.now(), transaction))
|
= notifyEvent(ServiceToClientEvent.Transaction(Instant.now(), transaction))
|
||||||
|
|
||||||
private fun processEventRequest(reqMessage: ClientToServiceCommandMessage) {
|
private fun processEventRequest(reqMessage: ClientToServiceCommandMessage) {
|
||||||
@ -94,11 +94,12 @@ class WalletMonitorService(services: ServiceHubInternal, val smm: StateMachineMa
|
|||||||
try {
|
try {
|
||||||
when (req) {
|
when (req) {
|
||||||
is ClientToServiceCommand.IssueCash -> issueCash(req)
|
is ClientToServiceCommand.IssueCash -> issueCash(req)
|
||||||
is ClientToServiceCommand.PayCash -> initatePayment(req)
|
is ClientToServiceCommand.PayCash -> initiatePayment(req)
|
||||||
is ClientToServiceCommand.ExitCash -> exitCash(req)
|
is ClientToServiceCommand.ExitCash -> exitCash(req)
|
||||||
else -> throw IllegalArgumentException("Unknown request type ${req.javaClass.name}")
|
else -> throw IllegalArgumentException("Unknown request type ${req.javaClass.name}")
|
||||||
}
|
}
|
||||||
} catch(ex: Exception) {
|
} catch(ex: Exception) {
|
||||||
|
logger.warn("Exception while processing message of type ${req.javaClass.simpleName}", ex)
|
||||||
TransactionBuildResult.Failed(ex.message)
|
TransactionBuildResult.Failed(ex.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,7 +135,7 @@ class WalletMonitorService(services: ServiceHubInternal, val smm: StateMachineMa
|
|||||||
fun processRegisterRequest(req: RegisterRequest) {
|
fun processRegisterRequest(req: RegisterRequest) {
|
||||||
try {
|
try {
|
||||||
listeners.add(RegisteredListener(req.replyToRecipient, req.sessionID))
|
listeners.add(RegisteredListener(req.replyToRecipient, req.sessionID))
|
||||||
val stateMessage = StateSnapshotMessage(services.walletService.currentWallet.states.map { it.state.data }.toList(),
|
val stateMessage = StateSnapshotMessage(services.walletService.currentWallet.states.toList(),
|
||||||
smm.allStateMachines.map { it.javaClass.name })
|
smm.allStateMachines.map { it.javaClass.name })
|
||||||
net.send(net.createMessage(STATE_TOPIC, DEFAULT_SESSION_ID, stateMessage.serialize().bits), req.replyToRecipient)
|
net.send(net.createMessage(STATE_TOPIC, DEFAULT_SESSION_ID, stateMessage.serialize().bits), req.replyToRecipient)
|
||||||
|
|
||||||
@ -151,7 +152,7 @@ class WalletMonitorService(services: ServiceHubInternal, val smm: StateMachineMa
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Make a lightweight protocol that manages this workflow, rather than embedding it directly in the service
|
// TODO: Make a lightweight protocol that manages this workflow, rather than embedding it directly in the service
|
||||||
private fun initatePayment(req: ClientToServiceCommand.PayCash): TransactionBuildResult {
|
private fun initiatePayment(req: ClientToServiceCommand.PayCash): TransactionBuildResult {
|
||||||
val builder: TransactionBuilder = TransactionType.General.Builder(null)
|
val builder: TransactionBuilder = TransactionType.General.Builder(null)
|
||||||
// TODO: Have some way of restricting this to states the caller controls
|
// TODO: Have some way of restricting this to states the caller controls
|
||||||
try {
|
try {
|
||||||
@ -165,7 +166,11 @@ class WalletMonitorService(services: ServiceHubInternal, val smm: StateMachineMa
|
|||||||
}
|
}
|
||||||
val tx = builder.toSignedTransaction(checkSufficientSignatures = false)
|
val tx = builder.toSignedTransaction(checkSufficientSignatures = false)
|
||||||
val protocol = FinalityProtocol(tx, setOf(req), setOf(req.recipient))
|
val protocol = FinalityProtocol(tx, setOf(req), setOf(req.recipient))
|
||||||
return TransactionBuildResult.ProtocolStarted(smm.add(BroadcastTransactionProtocol.TOPIC, protocol).machineId, tx, "Cash payment transaction generated")
|
return TransactionBuildResult.ProtocolStarted(
|
||||||
|
smm.add(BroadcastTransactionProtocol.TOPIC, protocol).machineId,
|
||||||
|
tx.tx.toLedgerTransaction(services),
|
||||||
|
"Cash payment transaction generated"
|
||||||
|
)
|
||||||
} catch(ex: InsufficientBalanceException) {
|
} catch(ex: InsufficientBalanceException) {
|
||||||
return TransactionBuildResult.Failed(ex.message ?: "Insufficient balance")
|
return TransactionBuildResult.Failed(ex.message ?: "Insufficient balance")
|
||||||
}
|
}
|
||||||
@ -174,6 +179,7 @@ class WalletMonitorService(services: ServiceHubInternal, val smm: StateMachineMa
|
|||||||
// TODO: Make a lightweight protocol that manages this workflow, rather than embedding it directly in the service
|
// TODO: Make a lightweight protocol that manages this workflow, rather than embedding it directly in the service
|
||||||
private fun exitCash(req: ClientToServiceCommand.ExitCash): TransactionBuildResult {
|
private fun exitCash(req: ClientToServiceCommand.ExitCash): TransactionBuildResult {
|
||||||
val builder: TransactionBuilder = TransactionType.General.Builder(null)
|
val builder: TransactionBuilder = TransactionType.General.Builder(null)
|
||||||
|
try {
|
||||||
val issuer = PartyAndReference(services.storageService.myLegalIdentity, req.issueRef)
|
val issuer = PartyAndReference(services.storageService.myLegalIdentity, req.issueRef)
|
||||||
Cash().generateExit(builder, req.amount.issuedBy(issuer),
|
Cash().generateExit(builder, req.amount.issuedBy(issuer),
|
||||||
services.walletService.currentWallet.statesOfType<Cash.State>().filter { it.state.data.owner == issuer.party.owningKey })
|
services.walletService.currentWallet.statesOfType<Cash.State>().filter { it.state.data.owner == issuer.party.owningKey })
|
||||||
@ -194,7 +200,14 @@ class WalletMonitorService(services: ServiceHubInternal, val smm: StateMachineMa
|
|||||||
// Commit the transaction
|
// Commit the transaction
|
||||||
val tx = builder.toSignedTransaction(checkSufficientSignatures = false)
|
val tx = builder.toSignedTransaction(checkSufficientSignatures = false)
|
||||||
val protocol = FinalityProtocol(tx, setOf(req), participants)
|
val protocol = FinalityProtocol(tx, setOf(req), participants)
|
||||||
return TransactionBuildResult.ProtocolStarted(smm.add(BroadcastTransactionProtocol.TOPIC, protocol).machineId, tx, "Cash destruction transaction generated")
|
return TransactionBuildResult.ProtocolStarted(
|
||||||
|
smm.add(BroadcastTransactionProtocol.TOPIC, protocol).machineId,
|
||||||
|
tx.tx.toLedgerTransaction(services),
|
||||||
|
"Cash destruction transaction generated"
|
||||||
|
)
|
||||||
|
} catch (ex: InsufficientBalanceException) {
|
||||||
|
return TransactionBuildResult.Failed(ex.message ?: "Insufficient balance")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Make a lightweight protocol that manages this workflow, rather than embedding it directly in the service
|
// TODO: Make a lightweight protocol that manages this workflow, rather than embedding it directly in the service
|
||||||
@ -206,7 +219,11 @@ class WalletMonitorService(services: ServiceHubInternal, val smm: StateMachineMa
|
|||||||
val tx = builder.toSignedTransaction(checkSufficientSignatures = true)
|
val tx = builder.toSignedTransaction(checkSufficientSignatures = true)
|
||||||
// Issuance transactions do not need to be notarised, so we can skip directly to broadcasting it
|
// Issuance transactions do not need to be notarised, so we can skip directly to broadcasting it
|
||||||
val protocol = BroadcastTransactionProtocol(tx, setOf(req), setOf(req.recipient))
|
val protocol = BroadcastTransactionProtocol(tx, setOf(req), setOf(req.recipient))
|
||||||
return TransactionBuildResult.ProtocolStarted(smm.add(BroadcastTransactionProtocol.TOPIC, protocol).machineId, tx, "Cash issuance completed")
|
return TransactionBuildResult.ProtocolStarted(
|
||||||
|
smm.add(BroadcastTransactionProtocol.TOPIC, protocol).machineId,
|
||||||
|
tx.tx.toLedgerTransaction(services),
|
||||||
|
"Cash issuance completed"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
class InputStateRefResolveFailed(stateRefs: List<StateRef>) :
|
class InputStateRefResolveFailed(stateRefs: List<StateRef>) :
|
||||||
|
@ -140,7 +140,7 @@ class WalletMonitorServiceTests {
|
|||||||
// Check the returned event is correct
|
// Check the returned event is correct
|
||||||
val tx = (event.state as TransactionBuildResult.ProtocolStarted).transaction
|
val tx = (event.state as TransactionBuildResult.ProtocolStarted).transaction
|
||||||
assertNotNull(tx)
|
assertNotNull(tx)
|
||||||
assertEquals(expectedState, tx!!.tx.outputs.single().data)
|
assertEquals(expectedState, tx!!.outputs.single().data)
|
||||||
},
|
},
|
||||||
expect { event: ServiceToClientEvent.OutputState ->
|
expect { event: ServiceToClientEvent.OutputState ->
|
||||||
// Check the generated state is correct
|
// Check the generated state is correct
|
||||||
@ -202,8 +202,8 @@ class WalletMonitorServiceTests {
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
expect { event: ServiceToClientEvent.Transaction ->
|
expect { event: ServiceToClientEvent.Transaction ->
|
||||||
require(event.transaction.sigs.size == 1)
|
require(event.transaction.mustSign.size == 1)
|
||||||
event.transaction.sigs.map { it.by }.toSet().containsAll(
|
event.transaction.mustSign.containsAll(
|
||||||
listOf(
|
listOf(
|
||||||
monitorServiceNode.services.storageService.myLegalIdentity.owningKey
|
monitorServiceNode.services.storageService.myLegalIdentity.owningKey
|
||||||
)
|
)
|
||||||
|
@ -7,3 +7,4 @@ include 'client'
|
|||||||
include 'experimental'
|
include 'experimental'
|
||||||
include 'test-utils'
|
include 'test-utils'
|
||||||
include 'network-simulator'
|
include 'network-simulator'
|
||||||
|
include 'explorer'
|
||||||
|