mirror of
https://github.com/corda/corda.git
synced 2024-12-21 05:53:23 +00:00
Merge branch 'master' into dynamic-loading
This commit is contained in:
commit
58a509de63
@ -112,7 +112,10 @@ dependencies {
|
||||
compile "io.dropwizard.metrics:metrics-core:3.1.2"
|
||||
|
||||
// JimFS: in memory java.nio filesystem. Used for test and simulation utilities.
|
||||
compile 'com.google.jimfs:jimfs:1.1'
|
||||
compile "com.google.jimfs:jimfs:1.1"
|
||||
|
||||
// TypeSafe Config: for simple and human friendly config files.
|
||||
compile "com.typesafe:config:1.3.0"
|
||||
|
||||
// Unit testing helpers.
|
||||
testCompile 'junit:junit:4.12'
|
||||
|
@ -225,7 +225,7 @@ fun LocalDate.isWorkingDay(accordingToCalendar: BusinessCalendar): Boolean = acc
|
||||
* typical feature of financial contracts, in which a business may not want a payment event to fall on a day when
|
||||
* no staff are around to handle problems.
|
||||
*/
|
||||
open class BusinessCalendar private constructor(val holidayDates: List<LocalDate>) {
|
||||
open class BusinessCalendar private constructor(val calendars: Array<out String>, val holidayDates: List<LocalDate>) {
|
||||
class UnknownCalendar(name: String): Exception("$name not found")
|
||||
|
||||
companion object {
|
||||
@ -239,7 +239,7 @@ open class BusinessCalendar private constructor(val holidayDates: List<LocalDate
|
||||
fun parseDateFromString(it: String) = LocalDate.parse(it, DateTimeFormatter.ISO_LOCAL_DATE)
|
||||
|
||||
/** Returns a business calendar that combines all the named holiday calendars into one list of holiday dates. */
|
||||
fun getInstance(vararg calname: String) = BusinessCalendar(
|
||||
fun getInstance(vararg calname: String) = BusinessCalendar(calname,
|
||||
calname.flatMap { (TEST_CALENDAR_DATA[it] ?: throw UnknownCalendar(it)).split(",") }.
|
||||
toSet().
|
||||
map{ parseDateFromString(it) }.
|
||||
|
@ -45,6 +45,23 @@ interface OwnableState : ContractState {
|
||||
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!) */
|
||||
fun ContractState.hash(): SecureHash = SecureHash.sha256(serialize().bits)
|
||||
|
||||
|
@ -42,9 +42,15 @@ import java.util.*
|
||||
*/
|
||||
class ProgressTracker(vararg steps: Step) {
|
||||
sealed class Change {
|
||||
class Position(val newStep: Step) : Change()
|
||||
class Rendering(val ofStep: Step) : Change()
|
||||
class Structural(val parent: Step) : Change()
|
||||
class Position(val newStep: Step) : Change() {
|
||||
override fun toString() = newStep.label
|
||||
}
|
||||
class Rendering(val ofStep: Step) : Change() {
|
||||
override fun toString() = ofStep.label
|
||||
}
|
||||
class Structural(val parent: Step) : Change() {
|
||||
override fun toString() = "Structural step change in child of ${parent.label}"
|
||||
}
|
||||
}
|
||||
|
||||
/** The superclass of all step objects. */
|
||||
|
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
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.core.JsonParseException
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.core.JsonToken
|
||||
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.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.LocalDateTime
|
||||
import javax.ws.rs.ext.ContextResolver
|
||||
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
|
||||
*/
|
||||
@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 {
|
||||
return defaultObjectMapper
|
||||
}
|
||||
|
||||
class ServiceHubObjectMapper(var serviceHub: ServiceHub): ObjectMapper() {
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun createDefaultMapper(): ObjectMapper {
|
||||
val mapper = ObjectMapper()
|
||||
private fun createDefaultMapper(services: ServiceHub): ObjectMapper {
|
||||
val mapper = ServiceHubObjectMapper(services)
|
||||
mapper.enable(SerializationFeature.INDENT_OUTPUT);
|
||||
mapper.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
|
||||
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")
|
||||
timeModule.addSerializer(LocalDate::class.java, ToStringSerializer)
|
||||
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(cordaModule)
|
||||
mapper.registerModule(KotlinModule())
|
||||
return mapper
|
||||
}
|
||||
@ -45,7 +75,69 @@ class Config: ContextResolver<ObjectMapper> {
|
||||
|
||||
object LocalDateDeserializer: JsonDeserializer<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
|
||||
|
||||
import core.Party
|
||||
import core.crypto.DummyPublicKey
|
||||
import java.util.*
|
||||
|
||||
/** 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 {
|
||||
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.
|
||||
class MockNetworkMapService : NetworkMapService {
|
||||
|
||||
data class MockAddress(val id: String): SingleMessageRecipient
|
||||
|
||||
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
|
||||
|
||||
import api.APIServer
|
||||
import api.APIServerImpl
|
||||
import com.codahale.metrics.MetricRegistry
|
||||
import contracts.*
|
||||
import core.*
|
||||
@ -30,6 +32,7 @@ import java.nio.file.FileAlreadyExistsException
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.security.KeyPair
|
||||
import java.time.Clock
|
||||
import java.util.*
|
||||
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
|
||||
* 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 {
|
||||
val PRIVATE_KEY_FILE_NAME = "identity-private-key"
|
||||
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 identityService: IdentityService get() = identity
|
||||
override val monitoringService: MonitoringService = MonitoringService(MetricRegistry())
|
||||
override val clock: Clock get() = platformClock
|
||||
}
|
||||
|
||||
val legallyIdentifableAddress: LegallyIdentifiableNode get() = LegallyIdentifiableNode(net.myAddress, storage.myLegalIdentity)
|
||||
@ -73,6 +77,7 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration,
|
||||
var inNodeTimestampingService: NodeTimestamperService? = null
|
||||
lateinit var identity: IdentityService
|
||||
lateinit var net: MessagingService
|
||||
lateinit var api: APIServer
|
||||
|
||||
open fun start(): AbstractNode {
|
||||
log.info("Node starting up ...")
|
||||
@ -83,6 +88,7 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration,
|
||||
wallet = NodeWalletService(services)
|
||||
keyManagement = E2ETestKeyManagementService()
|
||||
makeInterestRateOracleService()
|
||||
api = APIServerImpl(this)
|
||||
|
||||
// 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.
|
||||
@ -90,7 +96,7 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration,
|
||||
inNodeTimestampingService = null
|
||||
timestamperAddress
|
||||
} else {
|
||||
inNodeTimestampingService = NodeTimestamperService(net, storage.myLegalIdentity, storage.myLegalIdentityKey)
|
||||
inNodeTimestampingService = NodeTimestamperService(net, storage.myLegalIdentity, storage.myLegalIdentityKey, platformClock)
|
||||
LegallyIdentifiableNode(net.myAddress, storage.myLegalIdentity)
|
||||
}
|
||||
(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.ServletHolder
|
||||
import org.eclipse.jetty.webapp.WebAppContext
|
||||
import org.glassfish.jersey.server.ResourceConfig
|
||||
import org.glassfish.jersey.server.ServerProperties
|
||||
import org.glassfish.jersey.servlet.ServletContainer
|
||||
import java.io.RandomAccessFile
|
||||
@ -30,8 +31,8 @@ import java.nio.channels.FileLock
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.StandardOpenOption
|
||||
import java.time.Clock
|
||||
import javax.management.ObjectName
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
class ConfigurationException(message: String) : Exception(message)
|
||||
|
||||
@ -46,9 +47,11 @@ class ConfigurationException(message: String) : Exception(message)
|
||||
* have to specify that yourself.
|
||||
* @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 clock The clock used within the node and by all protocols etc
|
||||
*/
|
||||
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 {
|
||||
/** The port that is used by default if none is specified. As you know, 31337 is the most elite number. */
|
||||
val DEFAULT_PORT = 31337
|
||||
@ -88,14 +91,18 @@ class Node(dir: Path, val p2pAddr: HostAndPort, configuration: NodeConfiguration
|
||||
addServlet(DataUploadServlet::class.java, "/upload/*")
|
||||
addServlet(AttachmentDownloadServlet::class.java, "/attachments/*")
|
||||
|
||||
setAttribute("services", services)
|
||||
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
|
||||
val resourceConfig = ResourceConfig()
|
||||
// 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
|
||||
@ -103,11 +110,6 @@ class Node(dir: Path, val p2pAddr: HostAndPort, configuration: NodeConfiguration
|
||||
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 {
|
||||
alreadyRunningNodeCheck()
|
||||
super.start()
|
||||
@ -161,4 +163,4 @@ class Node(dir: Path, val p2pAddr: HostAndPort, configuration: NodeConfiguration
|
||||
if (nodeFileLock == null)
|
||||
nodeFileLock = RandomAccessFile(file, "rw").channel.lock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,37 +8,19 @@
|
||||
|
||||
package core.node
|
||||
|
||||
import java.util.*
|
||||
import kotlin.reflect.declaredMemberProperties
|
||||
import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
interface NodeConfiguration {
|
||||
val myLegalName: String
|
||||
val exportJMXto: String
|
||||
}
|
||||
|
||||
object DefaultConfiguration : NodeConfiguration {
|
||||
override val myLegalName: String = "Vast Global MegaCorp"
|
||||
override val exportJMXto: String = "" // can be "http" or empty
|
||||
// Allow the use of "String by config" syntax. TODO: Make it more flexible.
|
||||
operator fun Config.getValue(receiver: NodeConfigurationFromConfig, metadata: KProperty<*>) = getString(metadata.name)
|
||||
|
||||
fun toProperties(): Properties {
|
||||
val settings = DefaultConfiguration::class.declaredMemberProperties.map { it.name to it.get(this@DefaultConfiguration).toString() }
|
||||
val p = Properties().apply {
|
||||
for (setting in settings) {
|
||||
setProperty(setting.first, setting.second)
|
||||
}
|
||||
}
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple wrapper around a plain old Java .properties file. The keys have the same name as in the source code.
|
||||
*
|
||||
* TODO: Replace Java properties file with a better config file format (maybe yaml).
|
||||
* We want to be able to configure via a GUI too, so an ability to round-trip whitespace, comments etc when machine
|
||||
* editing the file is a must-have.
|
||||
*/
|
||||
class NodeConfigurationFromProperties(private val properties: Properties) : NodeConfiguration {
|
||||
override val myLegalName: String get() = properties.getProperty("myLegalName")
|
||||
override val exportJMXto: String get() = properties.getProperty("exportJMXto")
|
||||
class NodeConfigurationFromConfig(val config: Config = ConfigFactory.load()) : NodeConfiguration {
|
||||
override val myLegalName: String by config
|
||||
override val exportJMXto: String by config
|
||||
}
|
@ -11,6 +11,7 @@ package core.node.services
|
||||
import com.codahale.metrics.Gauge
|
||||
import contracts.Cash
|
||||
import core.*
|
||||
import core.crypto.SecureHash
|
||||
import core.utilities.loggerFor
|
||||
import core.utilities.trace
|
||||
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
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
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 {
|
||||
val ourNewStates = tx.outputs.
|
||||
filterIsInstance<OwnableState>().
|
||||
filter { it.owner in ourKeys }.
|
||||
map { tx.outRef<OwnableState>(it) }
|
||||
filter { isRelevant(it, ourKeys) }.
|
||||
map { tx.outRef<ContractState>(it) }
|
||||
|
||||
// Now calculate the states that are being spent by this transaction.
|
||||
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.PrivateKey
|
||||
import java.security.PublicKey
|
||||
import java.time.Clock
|
||||
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
|
||||
* 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")
|
||||
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>
|
||||
|
||||
/**
|
||||
* 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
|
||||
* new states that they create. You should only insert transactions that have been successfully verified here!
|
||||
@ -143,6 +158,7 @@ interface ServiceHub {
|
||||
val networkService: MessagingService
|
||||
val networkMapService: NetworkMapService
|
||||
val monitoringService: MonitoringService
|
||||
val clock: Clock
|
||||
|
||||
/**
|
||||
* Given a [LedgerTransaction], looks up all its dependencies in the local database, uses the identity service to map
|
||||
@ -156,4 +172,14 @@ interface ServiceHub {
|
||||
val ltxns = dependencies.map { it.verifyToLedgerTransaction(identityService, storageService.attachments) }
|
||||
TransactionGroup(setOf(ltx), ltxns.toSet()).verify()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.time.Clock
|
||||
import java.util.*
|
||||
import java.util.concurrent.ExecutorService
|
||||
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,
|
||||
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 serverThread: ExecutorService =
|
||||
if (mockNet.threadPerNode)
|
||||
|
@ -10,16 +10,16 @@ package demos
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.google.common.net.HostAndPort
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import contracts.CommercialPaper
|
||||
import core.*
|
||||
import core.crypto.SecureHash
|
||||
import core.crypto.generateKeyPair
|
||||
import core.messaging.LegallyIdentifiableNode
|
||||
import core.messaging.SingleMessageRecipient
|
||||
import core.node.DefaultConfiguration
|
||||
import core.node.Node
|
||||
import core.node.NodeConfiguration
|
||||
import core.node.NodeConfigurationFromProperties
|
||||
import core.node.NodeConfigurationFromConfig
|
||||
import core.node.services.ArtemisMessagingService
|
||||
import core.node.services.NodeAttachmentService
|
||||
import core.node.services.NodeWalletService
|
||||
@ -37,7 +37,6 @@ import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import kotlin.system.exitProcess
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
@ -291,11 +290,8 @@ private fun loadConfigFile(configFile: Path): NodeConfiguration {
|
||||
askAdminToEditConfig(configFile)
|
||||
}
|
||||
|
||||
val config = configFile.toFile().reader().use {
|
||||
NodeConfigurationFromProperties(
|
||||
Properties(DefaultConfiguration.toProperties()).apply { load(it) }
|
||||
)
|
||||
}
|
||||
System.setProperty("config.file", configFile.toAbsolutePath().toString())
|
||||
val config = NodeConfigurationFromConfig(ConfigFactory.load())
|
||||
|
||||
// Make sure admin did actually edit at least the legal name.
|
||||
if (config.myLegalName == defaultLegalName)
|
||||
|
@ -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
|
||||
// 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.
|
||||
serviceHub.storageService.validatedTransactions.putAll(downloadedSignedTxns.associateBy { it.id })
|
||||
serviceHub.recordTransactions(downloadedSignedTxns)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
|
2
src/main/resources/reference.conf
Normal file
2
src/main/resources/reference.conf
Normal file
@ -0,0 +1,2 @@
|
||||
myLegalName = "Vast Global MegaCorp, Ltd"
|
||||
exportJMXto = "http"
|
@ -14,14 +14,11 @@ import core.messaging.MessagingService
|
||||
import core.messaging.MockNetworkMapService
|
||||
import core.messaging.NetworkMapService
|
||||
import core.node.services.*
|
||||
import core.node.AbstractNode
|
||||
import core.node.services.StorageServiceImpl
|
||||
import core.serialization.SerializedBytes
|
||||
import core.serialization.deserialize
|
||||
import core.testutils.TEST_KEYS_TO_CORP_MAP
|
||||
import core.testutils.TEST_PROGRAM_MAP
|
||||
import core.testutils.TEST_TX_TIME
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
@ -76,6 +73,8 @@ class MockKeyManagementService(vararg initialKeys: KeyPair) : KeyManagementServi
|
||||
}
|
||||
|
||||
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>
|
||||
get() = TODO("Use NodeWalletService instead")
|
||||
|
||||
@ -125,7 +124,8 @@ class MockServices(
|
||||
val net: MessagingService? = null,
|
||||
val identity: IdentityService? = MockIdentityService,
|
||||
val storage: StorageService? = MockStorageService(),
|
||||
val networkMap: NetworkMapService? = MockNetworkMapService()
|
||||
val networkMap: NetworkMapService? = MockNetworkMapService(),
|
||||
val overrideClock: Clock? = Clock.systemUTC()
|
||||
) : ServiceHub {
|
||||
override val walletService: WalletService
|
||||
get() = wallet ?: throw UnsupportedOperationException()
|
||||
@ -139,6 +139,8 @@ class MockServices(
|
||||
get() = networkMap ?: throw UnsupportedOperationException()
|
||||
override val storageService: StorageService
|
||||
get() = storage ?: throw UnsupportedOperationException()
|
||||
override val clock: Clock
|
||||
get() = overrideClock ?: throw UnsupportedOperationException()
|
||||
|
||||
override val monitoringService: MonitoringService = MonitoringService(MetricRegistry())
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user