mirror of
https://github.com/corda/corda.git
synced 2025-06-17 22:58:19 +00:00
Basic JSON API with servertime call exposed at GET /api/servertime
Global Clock as part of ServiceHub to offer source of time in transactions, protocols, time stamping service etc (can be replace for demos and testing with a Clock that can be externally manipulated) Edited with Mike's feedback Edited with Mike's feedback
This commit is contained in:
@ -47,6 +47,23 @@ interface OwnableState : ContractState {
|
|||||||
fun withNewOwner(newOwner: PublicKey): Pair<CommandData, OwnableState>
|
fun withNewOwner(newOwner: PublicKey): Pair<CommandData, OwnableState>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A state that evolves by superseding itself, all of which share the common "thread"
|
||||||
|
*
|
||||||
|
* This simplifies the job of tracking the current version of certain types of state in e.g. a wallet
|
||||||
|
*/
|
||||||
|
interface LinearState: ContractState {
|
||||||
|
/** Unique thread id within the wallets of all parties */
|
||||||
|
val thread: SecureHash
|
||||||
|
|
||||||
|
/** Human readable well known reference (e.g. trade reference) */
|
||||||
|
// TODO we will push this down out of here once we have something more sophisticated and a more powerful query API
|
||||||
|
val ref: String
|
||||||
|
|
||||||
|
/** true if this should be tracked by our wallet(s) */
|
||||||
|
fun isRelevant(ourKeys: Set<PublicKey>): Boolean
|
||||||
|
}
|
||||||
|
|
||||||
/** Returns the SHA-256 hash of the serialised contents of this state (not cached!) */
|
/** Returns the SHA-256 hash of the serialised contents of this state (not cached!) */
|
||||||
fun ContractState.hash(): SecureHash = SecureHash.sha256(serialize().bits)
|
fun ContractState.hash(): SecureHash = SecureHash.sha256(serialize().bits)
|
||||||
|
|
||||||
|
141
src/main/kotlin/api/APIServer.kt
Normal file
141
src/main/kotlin/api/APIServer.kt
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members
|
||||||
|
* pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms
|
||||||
|
* set forth therein.
|
||||||
|
*
|
||||||
|
* All other rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package api
|
||||||
|
|
||||||
|
import core.ContractState
|
||||||
|
import core.SignedTransaction
|
||||||
|
import core.StateRef
|
||||||
|
import core.WireTransaction
|
||||||
|
import core.crypto.DigitalSignature
|
||||||
|
import core.crypto.SecureHash
|
||||||
|
import core.serialization.SerializedBytes
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import javax.ws.rs.GET
|
||||||
|
import javax.ws.rs.Path
|
||||||
|
import javax.ws.rs.Produces
|
||||||
|
import javax.ws.rs.core.MediaType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Top level interface to external interaction with the distributed ledger.
|
||||||
|
*
|
||||||
|
* Wherever a list is returned by a fetchXXX method that corresponds with an input list, that output list will have optional elements
|
||||||
|
* where a null indicates "missing" and the elements returned will be in the order corresponding with the input list.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@Path("")
|
||||||
|
interface APIServer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Report current UTC time as understood by the platform.
|
||||||
|
*/
|
||||||
|
@GET
|
||||||
|
@Path("servertime")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
fun serverTime(): LocalDateTime
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query your "local" states (containing only outputs involving you) and return the hashes & indexes associated with them
|
||||||
|
* to probably be later inflated by fetchLedgerTransactions() or fetchStates() although because immutable you can cache them
|
||||||
|
* to avoid calling fetchLedgerTransactions() many times.
|
||||||
|
*
|
||||||
|
* @param query Some "where clause" like expression.
|
||||||
|
* @return Zero or more matching States.
|
||||||
|
*/
|
||||||
|
fun queryStates(query: StatesQuery): List<StateRef>
|
||||||
|
|
||||||
|
fun fetchStates(states: List<StateRef>): Map<StateRef, ContractState?>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query for immutable transactions (results can be cached indefinitely by their id/hash).
|
||||||
|
*
|
||||||
|
* @param txs The hashes (from [StateRef.txhash] returned from [queryStates]) you would like full transactions for.
|
||||||
|
* @return null values indicate missing transactions from the requested list.
|
||||||
|
*/
|
||||||
|
fun fetchTransactions(txs: List<SecureHash>): Map<SecureHash, SignedTransaction?>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TransactionBuildSteps would be invocations of contract.generateXXX() methods that all share a common TransactionBuilder
|
||||||
|
* and a common contract type (e.g. Cash or CommercialPaper)
|
||||||
|
* which would automatically be passed as the first argument (we'd need that to be a criteria/pattern of the generateXXX methods).
|
||||||
|
*/
|
||||||
|
fun buildTransaction(type: ContractDefRef, steps: List<TransactionBuildStep>): SerializedBytes<WireTransaction>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a signature for this transaction signed by us.
|
||||||
|
*/
|
||||||
|
fun generateTransactionSignature(tx: SerializedBytes<WireTransaction>): DigitalSignature.WithKey
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to commit transaction (returned from build transaction) with the necessary signatures for that to be
|
||||||
|
* successful, otherwise exception is thrown.
|
||||||
|
*/
|
||||||
|
fun commitTransaction(tx: SerializedBytes<WireTransaction>, signatures: List<DigitalSignature.WithKey>): SecureHash
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method would not return until the protocol is finished (hence the "Sync").
|
||||||
|
*
|
||||||
|
* Longer term we'd add an Async version that returns some kind of ProtocolInvocationRef that could be queried and
|
||||||
|
* would appear on some kind of event message that is broadcast informing of progress.
|
||||||
|
*
|
||||||
|
* Will throw exception if protocol fails.
|
||||||
|
*/
|
||||||
|
fun invokeProtocolSync(type: ProtocolRef, args: Map<String, Any?>): Any?
|
||||||
|
|
||||||
|
// fun invokeProtocolAsync(type: ProtocolRef, args: Map<String, Any?>): ProtocolInstanceRef
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch protocols that require a response to some prompt/question by a human (on the "bank" side).
|
||||||
|
*/
|
||||||
|
fun fetchProtocolsRequiringAttention(query: StatesQuery): Map<StateRef, ProtocolRequiringAttention>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide the response that a protocol is waiting for.
|
||||||
|
*
|
||||||
|
* @param protocol Should refer to a previously supplied ProtocolRequiringAttention.
|
||||||
|
* @param stepId Which step of the protocol are we referring too.
|
||||||
|
* @param choice Should be one of the choices presented in the ProtocolRequiringAttention.
|
||||||
|
* @param args Any arguments required.
|
||||||
|
*/
|
||||||
|
fun provideProtocolResponse(protocol: ProtocolInstanceRef, choice: SecureHash, args: Map<String, Any?>)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encapsulates the contract type. e.g. Cash or CommercialPaper etc.
|
||||||
|
*/
|
||||||
|
interface ContractDefRef {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ContractClassRef(val className: String) : ContractDefRef
|
||||||
|
data class ContractLedgerRef(val hash: SecureHash) : ContractDefRef
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encapsulates the protocol to be instantiated. e.g. TwoPartyTradeProtocol.Buyer.
|
||||||
|
*/
|
||||||
|
interface ProtocolRef {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ProtocolClassRef(val className: String) : ProtocolRef
|
||||||
|
|
||||||
|
data class ProtocolInstanceRef(val protocolInstance: SecureHash, val protocolClass: ProtocolClassRef, val protocolStepId: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thinking that Instant is OK for short lived protocol deadlines.
|
||||||
|
*/
|
||||||
|
data class ProtocolRequiringAttention(val ref: ProtocolInstanceRef, val prompt: String, val choiceIdsToMessages: Map<SecureHash, String>, val dueBy: Instant)
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encapsulate a generateXXX method call on a contract.
|
||||||
|
*/
|
||||||
|
data class TransactionBuildStep(val generateMethodName: String, val args: Map<String, Any?>)
|
108
src/main/kotlin/api/APIServerImpl.kt
Normal file
108
src/main/kotlin/api/APIServerImpl.kt
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
|
import core.*
|
||||||
|
import core.crypto.DigitalSignature
|
||||||
|
import core.crypto.SecureHash
|
||||||
|
import core.node.AbstractNode
|
||||||
|
import core.protocols.ProtocolLogic
|
||||||
|
import core.serialization.SerializedBytes
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.reflect.KParameter
|
||||||
|
import kotlin.reflect.jvm.javaType
|
||||||
|
|
||||||
|
class APIServerImpl(val node: AbstractNode): APIServer {
|
||||||
|
|
||||||
|
override fun serverTime(): LocalDateTime = LocalDateTime.now(node.services.clock)
|
||||||
|
|
||||||
|
override fun queryStates(query: StatesQuery): List<StateRef> {
|
||||||
|
// We're going to hard code two options here for now and assume that all LinearStates are deals
|
||||||
|
// Would like to maybe move to a model where we take something like a JEXL string, although don't want to develop
|
||||||
|
// something we can't later implement against a persistent store (i.e. need to pick / build a query engine)
|
||||||
|
if (query is StatesQuery.Selection) {
|
||||||
|
if (query.criteria is StatesQuery.Criteria.AllDeals) {
|
||||||
|
val states = node.services.walletService.linearHeads
|
||||||
|
return states.values.map { it.ref }
|
||||||
|
}
|
||||||
|
else if (query.criteria is StatesQuery.Criteria.Deal) {
|
||||||
|
val states = node.services.walletService.linearHeadsInstanceOf(LinearState::class.java) {
|
||||||
|
it.ref == query.criteria.ref
|
||||||
|
}
|
||||||
|
return states.values.map { it.ref }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchStates(states: List<StateRef>): Map<StateRef, ContractState?> {
|
||||||
|
return node.services.walletService.statesForRefs(states)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchTransactions(txs: List<SecureHash>): Map<SecureHash, SignedTransaction?> {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun buildTransaction(type: ContractDefRef, steps: List<TransactionBuildStep>): SerializedBytes<WireTransaction> {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun generateTransactionSignature(tx: SerializedBytes<WireTransaction>): DigitalSignature.WithKey {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun commitTransaction(tx: SerializedBytes<WireTransaction>, signatures: List<DigitalSignature.WithKey>): SecureHash {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun invokeProtocolSync(type: ProtocolRef, args: Map<String, Any?>): Any? {
|
||||||
|
return invokeProtocolAsync(type, args).get()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun invokeProtocolAsync(type: ProtocolRef, args: Map<String, Any?>): ListenableFuture<out Any?> {
|
||||||
|
if(type is ProtocolClassRef) {
|
||||||
|
val clazz = Class.forName(type.className)
|
||||||
|
if(ProtocolLogic::class.java.isAssignableFrom(clazz)) {
|
||||||
|
// TODO for security, check annotated as exposed on API? Or have PublicProtocolLogic... etc
|
||||||
|
nextConstructor@ for (constructor in clazz.kotlin.constructors) {
|
||||||
|
val params = HashMap<KParameter, Any?>()
|
||||||
|
for (parameter in constructor.parameters) {
|
||||||
|
if (parameter.isOptional && !args.containsKey(parameter.name)) {
|
||||||
|
// OK to be missing
|
||||||
|
} else if (args.containsKey(parameter.name)) {
|
||||||
|
val value = args[parameter.name]
|
||||||
|
if (value is Any) {
|
||||||
|
if (!(parameter.type.javaType as Class<*>).isAssignableFrom(value.javaClass)) {
|
||||||
|
// Not null and not assignable
|
||||||
|
break@nextConstructor
|
||||||
|
}
|
||||||
|
} else if (!parameter.type.isMarkedNullable) {
|
||||||
|
// Null and not nullable
|
||||||
|
break@nextConstructor
|
||||||
|
}
|
||||||
|
params[parameter] = value
|
||||||
|
} else {
|
||||||
|
break@nextConstructor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If we get here then we matched every parameter
|
||||||
|
val protocol = constructor.callBy(params) as ProtocolLogic<*>
|
||||||
|
val future = node.smm.add("api-call",protocol)
|
||||||
|
return future
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw UnsupportedOperationException("Could not find matching protocol and constructor for: $type $args")
|
||||||
|
} else {
|
||||||
|
throw UnsupportedOperationException("Unsupported ProtocolRef type: $type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchProtocolsRequiringAttention(query: StatesQuery): Map<StateRef, ProtocolRequiringAttention> {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun provideProtocolResponse(protocol: ProtocolInstanceRef, choice: SecureHash, args: Map<String, Any?>) {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,11 +1,21 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonGenerator
|
import com.fasterxml.jackson.core.JsonGenerator
|
||||||
|
import com.fasterxml.jackson.core.JsonParseException
|
||||||
import com.fasterxml.jackson.core.JsonParser
|
import com.fasterxml.jackson.core.JsonParser
|
||||||
|
import com.fasterxml.jackson.core.JsonToken
|
||||||
import com.fasterxml.jackson.databind.*
|
import com.fasterxml.jackson.databind.*
|
||||||
|
import com.fasterxml.jackson.databind.deser.std.NumberDeserializers
|
||||||
|
import com.fasterxml.jackson.databind.deser.std.StringArrayDeserializer
|
||||||
import com.fasterxml.jackson.databind.module.SimpleModule
|
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||||
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
||||||
|
import core.BusinessCalendar
|
||||||
|
import core.Party
|
||||||
|
import core.crypto.SecureHash
|
||||||
|
import core.node.services.ServiceHub
|
||||||
|
import java.math.BigDecimal
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
|
import java.time.LocalDateTime
|
||||||
import javax.ws.rs.ext.ContextResolver
|
import javax.ws.rs.ext.ContextResolver
|
||||||
import javax.ws.rs.ext.Provider
|
import javax.ws.rs.ext.Provider
|
||||||
|
|
||||||
@ -14,24 +24,44 @@ import javax.ws.rs.ext.Provider
|
|||||||
* and to organise serializers / deserializers for java.time.* classes as necessary
|
* and to organise serializers / deserializers for java.time.* classes as necessary
|
||||||
*/
|
*/
|
||||||
@Provider
|
@Provider
|
||||||
class Config: ContextResolver<ObjectMapper> {
|
class Config(val services: ServiceHub): ContextResolver<ObjectMapper> {
|
||||||
|
|
||||||
val defaultObjectMapper = createDefaultMapper()
|
val defaultObjectMapper = createDefaultMapper(services)
|
||||||
|
|
||||||
override fun getContext(type: java.lang.Class<*>): ObjectMapper {
|
override fun getContext(type: java.lang.Class<*>): ObjectMapper {
|
||||||
return defaultObjectMapper
|
return defaultObjectMapper
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ServiceHubObjectMapper(var serviceHub: ServiceHub): ObjectMapper() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private fun createDefaultMapper(): ObjectMapper {
|
private fun createDefaultMapper(services: ServiceHub): ObjectMapper {
|
||||||
val mapper = ObjectMapper()
|
val mapper = ServiceHubObjectMapper(services)
|
||||||
mapper.enable(SerializationFeature.INDENT_OUTPUT);
|
mapper.enable(SerializationFeature.INDENT_OUTPUT);
|
||||||
|
mapper.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
|
||||||
mapper.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)
|
mapper.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)
|
||||||
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); // Although we shouldn't really use java.util.* but instead java.time.*
|
|
||||||
val timeModule = SimpleModule("java.time")
|
val timeModule = SimpleModule("java.time")
|
||||||
timeModule.addSerializer(LocalDate::class.java, ToStringSerializer)
|
timeModule.addSerializer(LocalDate::class.java, ToStringSerializer)
|
||||||
timeModule.addDeserializer(LocalDate::class.java, LocalDateDeserializer)
|
timeModule.addDeserializer(LocalDate::class.java, LocalDateDeserializer)
|
||||||
|
timeModule.addKeyDeserializer(LocalDate::class.java, LocalDateKeyDeserializer)
|
||||||
|
timeModule.addSerializer(LocalDateTime::class.java, ToStringSerializer)
|
||||||
|
|
||||||
|
val cordaModule = SimpleModule("core")
|
||||||
|
cordaModule.addSerializer(Party::class.java, PartySerializer)
|
||||||
|
cordaModule.addDeserializer(Party::class.java, PartyDeserializer)
|
||||||
|
cordaModule.addSerializer(BigDecimal::class.java, ToStringSerializer)
|
||||||
|
cordaModule.addDeserializer(BigDecimal::class.java, NumberDeserializers.BigDecimalDeserializer())
|
||||||
|
cordaModule.addSerializer(SecureHash::class.java, SecureHashSerializer)
|
||||||
|
// It's slightly remarkable, but apparently Jackson works out that this is the only possibility
|
||||||
|
// for a SecureHash at the moment and tries to use SHA256 directly even though we only give it SecureHash
|
||||||
|
cordaModule.addDeserializer(SecureHash.SHA256::class.java, SecureHashDeserializer())
|
||||||
|
cordaModule.addDeserializer(BusinessCalendar::class.java, CalendarDeserializer)
|
||||||
|
|
||||||
mapper.registerModule(timeModule)
|
mapper.registerModule(timeModule)
|
||||||
|
mapper.registerModule(cordaModule)
|
||||||
mapper.registerModule(KotlinModule())
|
mapper.registerModule(KotlinModule())
|
||||||
return mapper
|
return mapper
|
||||||
}
|
}
|
||||||
@ -45,7 +75,69 @@ class Config: ContextResolver<ObjectMapper> {
|
|||||||
|
|
||||||
object LocalDateDeserializer: JsonDeserializer<LocalDate>() {
|
object LocalDateDeserializer: JsonDeserializer<LocalDate>() {
|
||||||
override fun deserialize(parser: JsonParser, context: DeserializationContext): LocalDate {
|
override fun deserialize(parser: JsonParser, context: DeserializationContext): LocalDate {
|
||||||
return LocalDate.parse(parser.text)
|
return try {
|
||||||
|
LocalDate.parse(parser.text)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw JsonParseException("Invalid LocalDate ${parser.text}: ${e.message}", parser.currentLocation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object LocalDateKeyDeserializer: KeyDeserializer() {
|
||||||
|
override fun deserializeKey(text: String, p1: DeserializationContext): Any? {
|
||||||
|
return LocalDate.parse(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
object PartySerializer: JsonSerializer<Party>() {
|
||||||
|
override fun serialize(obj: Party, generator: JsonGenerator, provider: SerializerProvider) {
|
||||||
|
generator.writeString(obj.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object PartyDeserializer: JsonDeserializer<Party>() {
|
||||||
|
override fun deserialize(parser: JsonParser, context: DeserializationContext): Party {
|
||||||
|
if(parser.currentToken == JsonToken.FIELD_NAME) {
|
||||||
|
parser.nextToken()
|
||||||
|
}
|
||||||
|
val mapper = parser.codec as ServiceHubObjectMapper
|
||||||
|
// TODO this needs to use some industry identifier(s) not just these human readable names
|
||||||
|
val nodeForPartyName = mapper.serviceHub.networkMapService.nodeForPartyName(parser.text) ?: throw JsonParseException("Could not find a Party with name: ${parser.text}", parser.currentLocation)
|
||||||
|
return nodeForPartyName.identity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object SecureHashSerializer: JsonSerializer<SecureHash>() {
|
||||||
|
override fun serialize(obj: SecureHash, generator: JsonGenerator, provider: SerializerProvider) {
|
||||||
|
generator.writeString(obj.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implemented as a class so that we can instantiate for T
|
||||||
|
*/
|
||||||
|
class SecureHashDeserializer<T : SecureHash>: JsonDeserializer<T>() {
|
||||||
|
override fun deserialize(parser: JsonParser, context: DeserializationContext): T {
|
||||||
|
if(parser.currentToken == JsonToken.FIELD_NAME) {
|
||||||
|
parser.nextToken()
|
||||||
|
}
|
||||||
|
return try {
|
||||||
|
return SecureHash.parse(parser.text) as T
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw JsonParseException("Invalid hash ${parser.text}: ${e.message}", parser.currentLocation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object CalendarDeserializer: JsonDeserializer<BusinessCalendar>() {
|
||||||
|
override fun deserialize(parser: JsonParser, context: DeserializationContext): BusinessCalendar {
|
||||||
|
return try {
|
||||||
|
val array = StringArrayDeserializer.instance.deserialize(parser, context)
|
||||||
|
BusinessCalendar.getInstance(*array)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw JsonParseException("Invalid calendar(s) ${parser.text}: ${e.message}", parser.currentLocation)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
41
src/main/kotlin/api/Query.kt
Normal file
41
src/main/kotlin/api/Query.kt
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members
|
||||||
|
* pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms
|
||||||
|
* set forth therein.
|
||||||
|
*
|
||||||
|
* All other rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package api
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extremely rudimentary query language which should most likely be replaced with a product
|
||||||
|
*/
|
||||||
|
interface StatesQuery {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun select(criteria: Criteria): Selection {
|
||||||
|
return Selection(criteria)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectAllDeals(): Selection {
|
||||||
|
return select(Criteria.AllDeals)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectDeal(ref: String): Selection {
|
||||||
|
return select(Criteria.Deal(ref))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO make constructors private
|
||||||
|
data class Selection(val criteria: Criteria): StatesQuery
|
||||||
|
|
||||||
|
interface Criteria {
|
||||||
|
|
||||||
|
object AllDeals: Criteria
|
||||||
|
|
||||||
|
data class Deal(val ref: String): Criteria
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -9,6 +9,7 @@
|
|||||||
package core.messaging
|
package core.messaging
|
||||||
|
|
||||||
import core.Party
|
import core.Party
|
||||||
|
import core.crypto.DummyPublicKey
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
/** Info about a network node that has is operated by some sort of verified identity. */
|
/** Info about a network node that has is operated by some sort of verified identity. */
|
||||||
@ -25,9 +26,22 @@ data class LegallyIdentifiableNode(val address: SingleMessageRecipient, val iden
|
|||||||
*/
|
*/
|
||||||
interface NetworkMapService {
|
interface NetworkMapService {
|
||||||
val timestampingNodes: List<LegallyIdentifiableNode>
|
val timestampingNodes: List<LegallyIdentifiableNode>
|
||||||
|
val partyNodes: List<LegallyIdentifiableNode>
|
||||||
|
|
||||||
|
fun nodeForPartyName(name: String): LegallyIdentifiableNode? = partyNodes.singleOrNull { it.identity.name == name }
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Move this to the test tree once a real network map is implemented and this scaffolding is no longer needed.
|
// TODO: Move this to the test tree once a real network map is implemented and this scaffolding is no longer needed.
|
||||||
class MockNetworkMapService : NetworkMapService {
|
class MockNetworkMapService : NetworkMapService {
|
||||||
|
|
||||||
|
data class MockAddress(val id: String): SingleMessageRecipient
|
||||||
|
|
||||||
override val timestampingNodes = Collections.synchronizedList(ArrayList<LegallyIdentifiableNode>())
|
override val timestampingNodes = Collections.synchronizedList(ArrayList<LegallyIdentifiableNode>())
|
||||||
|
override val partyNodes = Collections.synchronizedList(ArrayList<LegallyIdentifiableNode>())
|
||||||
|
|
||||||
|
init {
|
||||||
|
partyNodes.add(LegallyIdentifiableNode(MockAddress("excalibur:8080"), Party("Excalibur", DummyPublicKey("Excalibur"))))
|
||||||
|
partyNodes.add(LegallyIdentifiableNode(MockAddress("another:8080"), Party("ANOther", DummyPublicKey("ANOther"))))
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,8 @@
|
|||||||
|
|
||||||
package core.node
|
package core.node
|
||||||
|
|
||||||
|
import api.APIServer
|
||||||
|
import api.APIServerImpl
|
||||||
import com.codahale.metrics.MetricRegistry
|
import com.codahale.metrics.MetricRegistry
|
||||||
import contracts.*
|
import contracts.*
|
||||||
import core.*
|
import core.*
|
||||||
@ -30,6 +32,7 @@ import java.nio.file.FileAlreadyExistsException
|
|||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.security.KeyPair
|
import java.security.KeyPair
|
||||||
|
import java.time.Clock
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
@ -37,7 +40,7 @@ import java.util.concurrent.Executors
|
|||||||
* A base node implementation that can be customised either for production (with real implementations that do real
|
* A base node implementation that can be customised either for production (with real implementations that do real
|
||||||
* I/O), or a mock implementation suitable for unit test environments.
|
* I/O), or a mock implementation suitable for unit test environments.
|
||||||
*/
|
*/
|
||||||
abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration, val timestamperAddress: LegallyIdentifiableNode?) {
|
abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration, val timestamperAddress: LegallyIdentifiableNode?, val platformClock: Clock) {
|
||||||
companion object {
|
companion object {
|
||||||
val PRIVATE_KEY_FILE_NAME = "identity-private-key"
|
val PRIVATE_KEY_FILE_NAME = "identity-private-key"
|
||||||
val PUBLIC_IDENTITY_FILE_NAME = "identity-public"
|
val PUBLIC_IDENTITY_FILE_NAME = "identity-public"
|
||||||
@ -62,6 +65,7 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration,
|
|||||||
override val keyManagementService: KeyManagementService get() = keyManagement
|
override val keyManagementService: KeyManagementService get() = keyManagement
|
||||||
override val identityService: IdentityService get() = identity
|
override val identityService: IdentityService get() = identity
|
||||||
override val monitoringService: MonitoringService = MonitoringService(MetricRegistry())
|
override val monitoringService: MonitoringService = MonitoringService(MetricRegistry())
|
||||||
|
override val clock: Clock get() = platformClock
|
||||||
}
|
}
|
||||||
|
|
||||||
val legallyIdentifableAddress: LegallyIdentifiableNode get() = LegallyIdentifiableNode(net.myAddress, storage.myLegalIdentity)
|
val legallyIdentifableAddress: LegallyIdentifiableNode get() = LegallyIdentifiableNode(net.myAddress, storage.myLegalIdentity)
|
||||||
@ -89,6 +93,7 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration,
|
|||||||
var inNodeTimestampingService: NodeTimestamperService? = null
|
var inNodeTimestampingService: NodeTimestamperService? = null
|
||||||
lateinit var identity: IdentityService
|
lateinit var identity: IdentityService
|
||||||
lateinit var net: MessagingService
|
lateinit var net: MessagingService
|
||||||
|
lateinit var api: APIServer
|
||||||
|
|
||||||
open fun start(): AbstractNode {
|
open fun start(): AbstractNode {
|
||||||
log.info("Node starting up ...")
|
log.info("Node starting up ...")
|
||||||
@ -99,6 +104,7 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration,
|
|||||||
wallet = NodeWalletService(services)
|
wallet = NodeWalletService(services)
|
||||||
keyManagement = E2ETestKeyManagementService()
|
keyManagement = E2ETestKeyManagementService()
|
||||||
makeInterestRateOracleService()
|
makeInterestRateOracleService()
|
||||||
|
api = APIServerImpl(this)
|
||||||
|
|
||||||
// Insert a network map entry for the timestamper: this is all temp scaffolding and will go away. If we are
|
// Insert a network map entry for the timestamper: this is all temp scaffolding and will go away. If we are
|
||||||
// given the details, the timestamping node is somewhere else. Otherwise, we do our own timestamping.
|
// given the details, the timestamping node is somewhere else. Otherwise, we do our own timestamping.
|
||||||
@ -106,7 +112,7 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration,
|
|||||||
inNodeTimestampingService = null
|
inNodeTimestampingService = null
|
||||||
timestamperAddress
|
timestamperAddress
|
||||||
} else {
|
} else {
|
||||||
inNodeTimestampingService = NodeTimestamperService(net, storage.myLegalIdentity, storage.myLegalIdentityKey)
|
inNodeTimestampingService = NodeTimestamperService(net, storage.myLegalIdentity, storage.myLegalIdentityKey, platformClock)
|
||||||
LegallyIdentifiableNode(net.myAddress, storage.myLegalIdentity)
|
LegallyIdentifiableNode(net.myAddress, storage.myLegalIdentity)
|
||||||
}
|
}
|
||||||
(services.networkMapService as MockNetworkMapService).timestampingNodes.add(tsid)
|
(services.networkMapService as MockNetworkMapService).timestampingNodes.add(tsid)
|
||||||
|
@ -22,6 +22,7 @@ import org.eclipse.jetty.server.handler.HandlerCollection
|
|||||||
import org.eclipse.jetty.servlet.ServletContextHandler
|
import org.eclipse.jetty.servlet.ServletContextHandler
|
||||||
import org.eclipse.jetty.servlet.ServletHolder
|
import org.eclipse.jetty.servlet.ServletHolder
|
||||||
import org.eclipse.jetty.webapp.WebAppContext
|
import org.eclipse.jetty.webapp.WebAppContext
|
||||||
|
import org.glassfish.jersey.server.ResourceConfig
|
||||||
import org.glassfish.jersey.server.ServerProperties
|
import org.glassfish.jersey.server.ServerProperties
|
||||||
import org.glassfish.jersey.servlet.ServletContainer
|
import org.glassfish.jersey.servlet.ServletContainer
|
||||||
import java.io.RandomAccessFile
|
import java.io.RandomAccessFile
|
||||||
@ -30,8 +31,8 @@ import java.nio.channels.FileLock
|
|||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.nio.file.StandardOpenOption
|
import java.nio.file.StandardOpenOption
|
||||||
|
import java.time.Clock
|
||||||
import javax.management.ObjectName
|
import javax.management.ObjectName
|
||||||
import kotlin.reflect.KClass
|
|
||||||
|
|
||||||
class ConfigurationException(message: String) : Exception(message)
|
class ConfigurationException(message: String) : Exception(message)
|
||||||
|
|
||||||
@ -46,9 +47,11 @@ class ConfigurationException(message: String) : Exception(message)
|
|||||||
* have to specify that yourself.
|
* have to specify that yourself.
|
||||||
* @param configuration This is typically loaded from a .properties file
|
* @param configuration This is typically loaded from a .properties file
|
||||||
* @param timestamperAddress If null, this node will become a timestamping node, otherwise, it will use that one.
|
* @param timestamperAddress If null, this node will become a timestamping node, otherwise, it will use that one.
|
||||||
|
* @param clock The clock used within the node and by all protocols etc
|
||||||
*/
|
*/
|
||||||
class Node(dir: Path, val p2pAddr: HostAndPort, configuration: NodeConfiguration,
|
class Node(dir: Path, val p2pAddr: HostAndPort, configuration: NodeConfiguration,
|
||||||
timestamperAddress: LegallyIdentifiableNode?) : AbstractNode(dir, configuration, timestamperAddress) {
|
timestamperAddress: LegallyIdentifiableNode?,
|
||||||
|
clock: Clock = Clock.systemUTC()) : AbstractNode(dir, configuration, timestamperAddress, clock) {
|
||||||
companion object {
|
companion object {
|
||||||
/** The port that is used by default if none is specified. As you know, 31337 is the most elite number. */
|
/** The port that is used by default if none is specified. As you know, 31337 is the most elite number. */
|
||||||
val DEFAULT_PORT = 31337
|
val DEFAULT_PORT = 31337
|
||||||
@ -88,14 +91,18 @@ class Node(dir: Path, val p2pAddr: HostAndPort, configuration: NodeConfiguration
|
|||||||
addServlet(DataUploadServlet::class.java, "/upload/*")
|
addServlet(DataUploadServlet::class.java, "/upload/*")
|
||||||
addServlet(AttachmentDownloadServlet::class.java, "/attachments/*")
|
addServlet(AttachmentDownloadServlet::class.java, "/attachments/*")
|
||||||
|
|
||||||
setAttribute("services", services)
|
val resourceConfig = ResourceConfig()
|
||||||
val jerseyServlet = addServlet(ServletContainer::class.java, "/api/*")
|
|
||||||
// Give the app a slightly better name in JMX rather than a randomly generated one
|
|
||||||
jerseyServlet.setInitParameter(ServerProperties.APPLICATION_NAME, "node.api")
|
|
||||||
jerseyServlet.setInitParameter(ServerProperties.MONITORING_STATISTICS_MBEANS_ENABLED, "true")
|
|
||||||
jerseyServlet.initOrder = 0 // Initialise at server start
|
|
||||||
// Add your API provider classes (annotated for JAX-RS) here
|
// Add your API provider classes (annotated for JAX-RS) here
|
||||||
setProviders(jerseyServlet, Config::class)
|
resourceConfig.register(Config(services))
|
||||||
|
resourceConfig.register(api)
|
||||||
|
// Give the app a slightly better name in JMX rather than a randomly generated one and enable JMX
|
||||||
|
resourceConfig.addProperties(mapOf(ServerProperties.APPLICATION_NAME to "node.api",
|
||||||
|
ServerProperties.MONITORING_STATISTICS_MBEANS_ENABLED to "true"))
|
||||||
|
|
||||||
|
val container = ServletContainer(resourceConfig)
|
||||||
|
val jerseyServlet = ServletHolder(container)
|
||||||
|
addServlet(jerseyServlet, "/api/*")
|
||||||
|
jerseyServlet.initOrder = 0 // Initialise at server start
|
||||||
})
|
})
|
||||||
|
|
||||||
server.handler = handlerCollection
|
server.handler = handlerCollection
|
||||||
@ -103,11 +110,6 @@ class Node(dir: Path, val p2pAddr: HostAndPort, configuration: NodeConfiguration
|
|||||||
return server
|
return server
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setProviders(jerseyServlet: ServletHolder, vararg providerClasses: KClass<out Any>) {
|
|
||||||
val providerClassNames = providerClasses.map { it.java.canonicalName }.joinToString()
|
|
||||||
jerseyServlet.setInitParameter(ServerProperties.PROVIDER_CLASSNAMES, providerClassNames)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun start(): Node {
|
override fun start(): Node {
|
||||||
alreadyRunningNodeCheck()
|
alreadyRunningNodeCheck()
|
||||||
super.start()
|
super.start()
|
||||||
@ -161,4 +163,4 @@ class Node(dir: Path, val p2pAddr: HostAndPort, configuration: NodeConfiguration
|
|||||||
if (nodeFileLock == null)
|
if (nodeFileLock == null)
|
||||||
nodeFileLock = RandomAccessFile(file, "rw").channel.lock()
|
nodeFileLock = RandomAccessFile(file, "rw").channel.lock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ package core.node.services
|
|||||||
import com.codahale.metrics.Gauge
|
import com.codahale.metrics.Gauge
|
||||||
import contracts.Cash
|
import contracts.Cash
|
||||||
import core.*
|
import core.*
|
||||||
|
import core.crypto.SecureHash
|
||||||
import core.utilities.loggerFor
|
import core.utilities.loggerFor
|
||||||
import core.utilities.trace
|
import core.utilities.trace
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
@ -42,6 +43,14 @@ class NodeWalletService(private val services: ServiceHub) : WalletService {
|
|||||||
*/
|
*/
|
||||||
override val cashBalances: Map<Currency, Amount> get() = mutex.locked { wallet }.cashBalances
|
override val cashBalances: Map<Currency, Amount> get() = mutex.locked { wallet }.cashBalances
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a snapshot of the heads of LinearStates
|
||||||
|
*/
|
||||||
|
override val linearHeads: Map<SecureHash, StateAndRef<LinearState>>
|
||||||
|
get() = mutex.locked { wallet }.let { wallet ->
|
||||||
|
wallet.states.filter { it.state is LinearState }.associateBy { (it.state as LinearState).thread }.mapValues { it.value as StateAndRef<LinearState> }
|
||||||
|
}
|
||||||
|
|
||||||
override fun notifyAll(txns: Iterable<WireTransaction>): Wallet {
|
override fun notifyAll(txns: Iterable<WireTransaction>): Wallet {
|
||||||
val ourKeys = services.keyManagementService.keys.keys
|
val ourKeys = services.keyManagementService.keys.keys
|
||||||
|
|
||||||
@ -68,11 +77,21 @@ class NodeWalletService(private val services: ServiceHub) : WalletService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isRelevant(state: ContractState, ourKeys: Set<PublicKey>): Boolean {
|
||||||
|
return if(state is OwnableState) {
|
||||||
|
state.owner in ourKeys
|
||||||
|
} else if(state is LinearState) {
|
||||||
|
// It's potentially of interest to the wallet
|
||||||
|
state.isRelevant(ourKeys)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun Wallet.update(tx: WireTransaction, ourKeys: Set<PublicKey>): Wallet {
|
private fun Wallet.update(tx: WireTransaction, ourKeys: Set<PublicKey>): Wallet {
|
||||||
val ourNewStates = tx.outputs.
|
val ourNewStates = tx.outputs.
|
||||||
filterIsInstance<OwnableState>().
|
filter { isRelevant(it, ourKeys) }.
|
||||||
filter { it.owner in ourKeys }.
|
map { tx.outRef<ContractState>(it) }
|
||||||
map { tx.outRef<OwnableState>(it) }
|
|
||||||
|
|
||||||
// Now calculate the states that are being spent by this transaction.
|
// Now calculate the states that are being spent by this transaction.
|
||||||
val consumed: Set<StateRef> = states.map { it.ref }.intersect(tx.inputs)
|
val consumed: Set<StateRef> = states.map { it.ref }.intersect(tx.inputs)
|
||||||
|
@ -18,6 +18,7 @@ import java.io.InputStream
|
|||||||
import java.security.KeyPair
|
import java.security.KeyPair
|
||||||
import java.security.PrivateKey
|
import java.security.PrivateKey
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
|
import java.time.Clock
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -31,7 +32,7 @@ import java.util.*
|
|||||||
* change out from underneath you, even though the canonical currently-best-known wallet may change as we learn
|
* change out from underneath you, even though the canonical currently-best-known wallet may change as we learn
|
||||||
* about new transactions from our peers and generate new transactions that consume states ourselves.
|
* about new transactions from our peers and generate new transactions that consume states ourselves.
|
||||||
*/
|
*/
|
||||||
data class Wallet(val states: List<StateAndRef<OwnableState>>) {
|
data class Wallet(val states: List<StateAndRef<ContractState>>) {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
inline fun <reified T : OwnableState> statesOfType() = states.filter { it.state is T } as List<StateAndRef<T>>
|
inline fun <reified T : OwnableState> statesOfType() = states.filter { it.state is T } as List<StateAndRef<T>>
|
||||||
|
|
||||||
@ -67,6 +68,20 @@ interface WalletService {
|
|||||||
*/
|
*/
|
||||||
val cashBalances: Map<Currency, Amount>
|
val cashBalances: Map<Currency, Amount>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a snapshot of the heads of LinearStates
|
||||||
|
*/
|
||||||
|
val linearHeads: Map<SecureHash, StateAndRef<LinearState>>
|
||||||
|
|
||||||
|
fun <T : LinearState> linearHeadsInstanceOf(clazz: Class<T>, predicate: (T) -> Boolean = { true } ): Map<SecureHash, StateAndRef<LinearState>> {
|
||||||
|
return linearHeads.filterValues { clazz.isInstance(it.state) }.filterValues { predicate(it.state as T) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun statesForRefs(refs: List<StateRef>): Map<StateRef, ContractState?> {
|
||||||
|
val refsToStates = currentWallet.states.associateBy { it.ref }
|
||||||
|
return refs.associateBy( { it }, { refsToStates[it]?.state } )
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Possibly update the wallet by marking as spent states that these transactions consume, and adding any relevant
|
* Possibly update the wallet by marking as spent states that these transactions consume, and adding any relevant
|
||||||
* new states that they create. You should only insert transactions that have been successfully verified here!
|
* new states that they create. You should only insert transactions that have been successfully verified here!
|
||||||
@ -175,6 +190,7 @@ interface ServiceHub {
|
|||||||
val networkService: MessagingService
|
val networkService: MessagingService
|
||||||
val networkMapService: NetworkMapService
|
val networkMapService: NetworkMapService
|
||||||
val monitoringService: MonitoringService
|
val monitoringService: MonitoringService
|
||||||
|
val clock: Clock
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a [LedgerTransaction], looks up all its dependencies in the local database, uses the identity service to map
|
* Given a [LedgerTransaction], looks up all its dependencies in the local database, uses the identity service to map
|
||||||
@ -188,4 +204,14 @@ interface ServiceHub {
|
|||||||
val ltxns = dependencies.map { it.verifyToLedgerTransaction(identityService, storageService.attachments) }
|
val ltxns = dependencies.map { it.verifyToLedgerTransaction(identityService, storageService.attachments) }
|
||||||
TransactionGroup(setOf(ltx), ltxns.toSet()).verify(storageService.contractPrograms)
|
TransactionGroup(setOf(ltx), ltxns.toSet()).verify(storageService.contractPrograms)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this for storing transactions to StorageService and WalletService
|
||||||
|
*
|
||||||
|
* TODO Need to come up with a way for preventing transactions being written other than by this method
|
||||||
|
*/
|
||||||
|
fun recordTransactions(txs: List<SignedTransaction>) {
|
||||||
|
storageService.validatedTransactions.putAll(txs.groupBy { it.id }.mapValues { it.value.first() })
|
||||||
|
walletService.notifyAll(txs.map { it.tx })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ import core.utilities.loggerFor
|
|||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
|
import java.time.Clock
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.ExecutorService
|
import java.util.concurrent.ExecutorService
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
@ -48,7 +49,7 @@ class MockNetwork(private val threadPerNode: Boolean = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
open class MockNode(dir: Path, config: NodeConfiguration, val mockNet: MockNetwork,
|
open class MockNode(dir: Path, config: NodeConfiguration, val mockNet: MockNetwork,
|
||||||
withTimestamper: LegallyIdentifiableNode?, val forcedID: Int = -1) : AbstractNode(dir, config, withTimestamper) {
|
withTimestamper: LegallyIdentifiableNode?, val forcedID: Int = -1) : AbstractNode(dir, config, withTimestamper, Clock.systemUTC()) {
|
||||||
override val log: Logger = loggerFor<MockNode>()
|
override val log: Logger = loggerFor<MockNode>()
|
||||||
override val serverThread: ExecutorService =
|
override val serverThread: ExecutorService =
|
||||||
if (mockNet.threadPerNode)
|
if (mockNet.threadPerNode)
|
||||||
|
@ -75,7 +75,7 @@ class ResolveTransactionsProtocol(private val txHashes: Set<SecureHash>,
|
|||||||
// It may seem tempting to write transactions to the database as we receive them, instead of all at once
|
// It may seem tempting to write transactions to the database as we receive them, instead of all at once
|
||||||
// here at the end. Doing it this way avoids cases where a transaction is in the database but its
|
// here at the end. Doing it this way avoids cases where a transaction is in the database but its
|
||||||
// dependencies aren't, or an unvalidated and possibly broken tx is there.
|
// dependencies aren't, or an unvalidated and possibly broken tx is there.
|
||||||
serviceHub.storageService.validatedTransactions.putAll(downloadedSignedTxns.associateBy { it.id })
|
serviceHub.recordTransactions(downloadedSignedTxns)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
|
@ -14,14 +14,11 @@ import core.messaging.MessagingService
|
|||||||
import core.messaging.MockNetworkMapService
|
import core.messaging.MockNetworkMapService
|
||||||
import core.messaging.NetworkMapService
|
import core.messaging.NetworkMapService
|
||||||
import core.node.services.*
|
import core.node.services.*
|
||||||
import core.node.AbstractNode
|
|
||||||
import core.node.services.StorageServiceImpl
|
|
||||||
import core.serialization.SerializedBytes
|
import core.serialization.SerializedBytes
|
||||||
import core.serialization.deserialize
|
import core.serialization.deserialize
|
||||||
import core.testutils.TEST_KEYS_TO_CORP_MAP
|
import core.testutils.TEST_KEYS_TO_CORP_MAP
|
||||||
import core.testutils.TEST_PROGRAM_MAP
|
import core.testutils.TEST_PROGRAM_MAP
|
||||||
import core.testutils.TEST_TX_TIME
|
import core.testutils.TEST_TX_TIME
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -76,6 +73,8 @@ class MockKeyManagementService(vararg initialKeys: KeyPair) : KeyManagementServi
|
|||||||
}
|
}
|
||||||
|
|
||||||
class MockWalletService(val states: List<StateAndRef<OwnableState>>) : WalletService {
|
class MockWalletService(val states: List<StateAndRef<OwnableState>>) : WalletService {
|
||||||
|
override val linearHeads: Map<SecureHash, StateAndRef<LinearState>>
|
||||||
|
get() = TODO("Use NodeWalletService instead")
|
||||||
override val cashBalances: Map<Currency, Amount>
|
override val cashBalances: Map<Currency, Amount>
|
||||||
get() = TODO("Use NodeWalletService instead")
|
get() = TODO("Use NodeWalletService instead")
|
||||||
|
|
||||||
@ -132,7 +131,8 @@ class MockServices(
|
|||||||
val net: MessagingService? = null,
|
val net: MessagingService? = null,
|
||||||
val identity: IdentityService? = MockIdentityService,
|
val identity: IdentityService? = MockIdentityService,
|
||||||
val storage: StorageService? = MockStorageService(),
|
val storage: StorageService? = MockStorageService(),
|
||||||
val networkMap: NetworkMapService? = MockNetworkMapService()
|
val networkMap: NetworkMapService? = MockNetworkMapService(),
|
||||||
|
val overrideClock: Clock? = Clock.systemUTC()
|
||||||
) : ServiceHub {
|
) : ServiceHub {
|
||||||
override val walletService: WalletService
|
override val walletService: WalletService
|
||||||
get() = wallet ?: throw UnsupportedOperationException()
|
get() = wallet ?: throw UnsupportedOperationException()
|
||||||
@ -146,6 +146,8 @@ class MockServices(
|
|||||||
get() = networkMap ?: throw UnsupportedOperationException()
|
get() = networkMap ?: throw UnsupportedOperationException()
|
||||||
override val storageService: StorageService
|
override val storageService: StorageService
|
||||||
get() = storage ?: throw UnsupportedOperationException()
|
get() = storage ?: throw UnsupportedOperationException()
|
||||||
|
override val clock: Clock
|
||||||
|
get() = overrideClock ?: throw UnsupportedOperationException()
|
||||||
|
|
||||||
override val monitoringService: MonitoringService = MonitoringService(MetricRegistry())
|
override val monitoringService: MonitoringService = MonitoringService(MetricRegistry())
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user