mirror of
https://github.com/corda/corda.git
synced 2025-06-16 22:28:15 +00:00
Refactor code into clear core, contracts and node namespaces. Move services into clear implementation and api sides. Push unit tests down to lowest level of dependency hierarchy possible.
This commit is contained in:
@ -1,24 +0,0 @@
|
||||
package core.testing
|
||||
|
||||
import core.crypto.Party
|
||||
import core.node.services.IdentityService
|
||||
import java.security.PublicKey
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
|
||||
/**
|
||||
* Scaffolding: a dummy identity service that just expects to have identities loaded off disk or found elsewhere.
|
||||
* This class allows the provided list of identities to be mutated after construction, so it takes the list lock
|
||||
* when doing lookups and recalculates the mapping each time. The ability to change the list is used by the
|
||||
* MockNetwork code.
|
||||
*/
|
||||
@ThreadSafe
|
||||
class MockIdentityService(val identities: List<Party>) : IdentityService {
|
||||
private val keyToParties: Map<PublicKey, Party>
|
||||
get() = synchronized(identities) { identities.associateBy { it.owningKey } }
|
||||
private val nameToParties: Map<String, Party>
|
||||
get() = synchronized(identities) { identities.associateBy { it.name } }
|
||||
|
||||
override fun registerIdentity(party: Party) { throw UnsupportedOperationException() }
|
||||
override fun partyFromKey(key: PublicKey): Party? = keyToParties[key]
|
||||
override fun partyFromName(name: String): Party? = nameToParties[name]
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package api
|
||||
package node.api
|
||||
|
||||
import node.api.StatesQuery
|
||||
import core.contracts.ContractState
|
||||
import core.contracts.SignedTransaction
|
||||
import core.contracts.StateRef
|
@ -1,4 +1,4 @@
|
||||
package api
|
||||
package node.api
|
||||
|
||||
/**
|
||||
* Extremely rudimentary query language which should most likely be replaced with a product
|
@ -1,15 +1,14 @@
|
||||
package api
|
||||
package node.core
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import core.*
|
||||
import core.contracts.*
|
||||
import core.crypto.DigitalSignature
|
||||
import core.crypto.SecureHash
|
||||
import core.node.AbstractNode
|
||||
import core.node.subsystems.linearHeadsOfType
|
||||
import core.node.services.linearHeadsOfType
|
||||
import core.protocols.ProtocolLogic
|
||||
import core.serialization.SerializedBytes
|
||||
import core.utilities.ANSIProgressRenderer
|
||||
import node.api.*
|
||||
import node.utilities.*
|
||||
import java.time.LocalDateTime
|
||||
import java.util.*
|
||||
import kotlin.reflect.KParameter
|
@ -1,25 +1,44 @@
|
||||
package core.node
|
||||
package node.core
|
||||
|
||||
import api.APIServer
|
||||
import api.APIServerImpl
|
||||
import com.codahale.metrics.MetricRegistry
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import core.RunOnCallerThread
|
||||
import core.crypto.Party
|
||||
import core.messaging.MessagingService
|
||||
import core.messaging.StateMachineManager
|
||||
import core.messaging.runOnNextMessage
|
||||
import core.node.CityDatabase
|
||||
import core.node.NodeInfo
|
||||
import core.node.PhysicalLocation
|
||||
import core.node.services.*
|
||||
import core.node.storage.CheckpointStorage
|
||||
import core.node.storage.PerFileCheckpointStorage
|
||||
import core.node.subsystems.*
|
||||
import core.random63BitValue
|
||||
import core.seconds
|
||||
import core.serialization.deserialize
|
||||
import core.serialization.serialize
|
||||
import core.utilities.AddOrRemove
|
||||
import core.utilities.AffinityExecutor
|
||||
import node.api.APIServer
|
||||
import node.services.api.AcceptsFileUpload
|
||||
import node.services.api.CheckpointStorage
|
||||
import node.services.api.MonitoringService
|
||||
import node.services.api.ServiceHubInternal
|
||||
import node.services.transactions.InMemoryUniquenessProvider
|
||||
import node.services.transactions.NotaryService
|
||||
import node.services.transactions.TimestampChecker
|
||||
import node.services.clientapi.NodeInterestRates
|
||||
import node.services.config.NodeConfiguration
|
||||
import node.services.identity.InMemoryIdentityService
|
||||
import node.services.keys.E2ETestKeyManagementService
|
||||
import node.services.network.InMemoryNetworkMapCache
|
||||
import node.services.network.InMemoryNetworkMapService
|
||||
import node.services.network.NetworkMapService
|
||||
import node.services.network.NodeRegistration
|
||||
import node.services.persistence.DataVendingService
|
||||
import node.services.persistence.NodeAttachmentService
|
||||
import node.services.persistence.PerFileCheckpointStorage
|
||||
import node.services.persistence.StorageServiceImpl
|
||||
import node.services.statemachine.StateMachineManager
|
||||
import node.services.wallet.NodeWalletService
|
||||
import node.utilities.AddOrRemove
|
||||
import node.utilities.AffinityExecutor
|
||||
import org.slf4j.Logger
|
||||
import java.nio.file.FileAlreadyExistsException
|
||||
import java.nio.file.Files
|
||||
@ -60,7 +79,7 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration,
|
||||
protected val _servicesThatAcceptUploads = ArrayList<AcceptsFileUpload>()
|
||||
val servicesThatAcceptUploads: List<AcceptsFileUpload> = _servicesThatAcceptUploads
|
||||
|
||||
val services = object : ServiceHub {
|
||||
val services = object : ServiceHubInternal {
|
||||
override val networkService: MessagingService get() = net
|
||||
override val networkMapCache: NetworkMapCache = InMemoryNetworkMapCache()
|
||||
override val storageService: StorageService get() = storage
|
||||
@ -219,7 +238,7 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration,
|
||||
|
||||
protected abstract fun startMessagingService()
|
||||
|
||||
protected open fun initialiseStorageService(dir: Path): Pair<StorageService,CheckpointStorage> {
|
||||
protected open fun initialiseStorageService(dir: Path): Pair<StorageService, CheckpointStorage> {
|
||||
val attachments = makeAttachmentStorage(dir)
|
||||
val checkpointStorage = PerFileCheckpointStorage(dir.resolve("checkpoints"))
|
||||
_servicesThatAcceptUploads += attachments
|
@ -1,16 +1,19 @@
|
||||
package core.node
|
||||
package node.core
|
||||
|
||||
import api.Config
|
||||
import api.ResponseFilter
|
||||
import com.codahale.metrics.JmxReporter
|
||||
import com.google.common.net.HostAndPort
|
||||
import core.messaging.MessagingService
|
||||
import core.node.subsystems.ArtemisMessagingService
|
||||
import core.node.NodeInfo
|
||||
import core.node.services.ServiceType
|
||||
import core.node.servlets.AttachmentDownloadServlet
|
||||
import core.node.servlets.DataUploadServlet
|
||||
import core.utilities.AffinityExecutor
|
||||
import core.utilities.loggerFor
|
||||
import node.api.APIServer
|
||||
import node.services.config.NodeConfiguration
|
||||
import node.services.messaging.ArtemisMessagingService
|
||||
import node.servlets.AttachmentDownloadServlet
|
||||
import node.servlets.Config
|
||||
import node.servlets.DataUploadServlet
|
||||
import node.servlets.ResponseFilter
|
||||
import node.utilities.AffinityExecutor
|
||||
import org.eclipse.jetty.server.Server
|
||||
import org.eclipse.jetty.server.handler.HandlerCollection
|
||||
import org.eclipse.jetty.servlet.ServletContextHandler
|
||||
@ -48,7 +51,7 @@ class ConfigurationException(message: String) : Exception(message)
|
||||
* @param clock The clock used within the node and by all protocols etc
|
||||
*/
|
||||
class Node(dir: Path, val p2pAddr: HostAndPort, configuration: NodeConfiguration,
|
||||
networkMapAddress: NodeInfo?,advertisedServices: Set<ServiceType>,
|
||||
networkMapAddress: NodeInfo?, advertisedServices: Set<ServiceType>,
|
||||
clock: Clock = Clock.systemUTC(),
|
||||
val clientAPIs: List<Class<*>> = listOf()) : AbstractNode(dir, configuration, networkMapAddress, advertisedServices, clock) {
|
||||
companion object {
|
||||
@ -106,7 +109,7 @@ class Node(dir: Path, val p2pAddr: HostAndPort, configuration: NodeConfiguration
|
||||
resourceConfig.register(api)
|
||||
|
||||
for(customAPIClass in clientAPIs) {
|
||||
val customAPI = customAPIClass.getConstructor(api.APIServer::class.java).newInstance(api)
|
||||
val customAPI = customAPIClass.getConstructor(APIServer::class.java).newInstance(api)
|
||||
resourceConfig.register(customAPI)
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
package core.testing
|
||||
package node.core.testing
|
||||
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.google.common.util.concurrent.FutureCallback
|
||||
@ -6,12 +6,17 @@ import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import contracts.InterestRateSwap
|
||||
import core.*
|
||||
import core.RunOnCallerThread
|
||||
import core.contracts.SignedTransaction
|
||||
import core.contracts.StateAndRef
|
||||
import core.crypto.SecureHash
|
||||
import core.node.subsystems.linearHeadsOfType
|
||||
import core.utilities.JsonSupport
|
||||
import core.failure
|
||||
import core.node.services.linearHeadsOfType
|
||||
import core.node.services.testing.MockIdentityService
|
||||
import core.random63BitValue
|
||||
import core.success
|
||||
import node.services.network.InMemoryMessagingNetwork
|
||||
import node.utilities.JsonSupport
|
||||
import protocols.TwoPartyDealProtocol
|
||||
import java.security.KeyPair
|
||||
import java.time.LocalDate
|
@ -1,19 +1,22 @@
|
||||
package core.testing
|
||||
package node.core.testing
|
||||
|
||||
import com.google.common.jimfs.Configuration
|
||||
import com.google.common.jimfs.Jimfs
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import core.crypto.Party
|
||||
import core.messaging.MessagingService
|
||||
import core.messaging.SingleMessageRecipient
|
||||
import core.node.AbstractNode
|
||||
import core.node.NodeConfiguration
|
||||
import core.node.NodeInfo
|
||||
import core.node.PhysicalLocation
|
||||
import core.node.services.NetworkMapService
|
||||
import core.node.services.NotaryService
|
||||
import core.node.services.ServiceType
|
||||
import core.utilities.AffinityExecutor
|
||||
import core.node.services.testing.MockIdentityService
|
||||
import core.utilities.loggerFor
|
||||
import node.core.AbstractNode
|
||||
import node.services.config.NodeConfiguration
|
||||
import node.services.network.InMemoryMessagingNetwork
|
||||
import node.services.network.NetworkMapService
|
||||
import node.services.transactions.NotaryService
|
||||
import node.utilities.AffinityExecutor
|
||||
import org.slf4j.Logger
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
@ -36,7 +39,7 @@ import java.util.*
|
||||
class MockNetwork(private val threadPerNode: Boolean = false,
|
||||
private val defaultFactory: Factory = MockNetwork.DefaultFactory) {
|
||||
private var counter = 0
|
||||
val filesystem = Jimfs.newFileSystem(com.google.common.jimfs.Configuration.unix())
|
||||
val filesystem = Jimfs.newFileSystem(Configuration.unix())
|
||||
val messagingNetwork = InMemoryMessagingNetwork()
|
||||
|
||||
val identities = ArrayList<Party>()
|
@ -1,18 +1,19 @@
|
||||
package core.testing
|
||||
package node.core.testing
|
||||
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import core.node.CityDatabase
|
||||
import core.node.NodeConfiguration
|
||||
import core.node.NodeInfo
|
||||
import core.node.PhysicalLocation
|
||||
import core.node.services.NetworkMapService
|
||||
import core.node.services.NodeInterestRates
|
||||
import core.node.services.NotaryService
|
||||
import core.node.services.ServiceType
|
||||
import core.protocols.ProtocolLogic
|
||||
import core.then
|
||||
import core.utilities.ProgressTracker
|
||||
import node.services.transactions.NotaryService
|
||||
import node.services.clientapi.NodeInterestRates
|
||||
import node.services.config.NodeConfiguration
|
||||
import node.services.network.InMemoryMessagingNetwork
|
||||
import node.services.network.NetworkMapService
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
import java.nio.file.Path
|
20
node/src/main/kotlin/node/core/testing/TestUtils.kt
Normal file
20
node/src/main/kotlin/node/core/testing/TestUtils.kt
Normal file
@ -0,0 +1,20 @@
|
||||
@file:Suppress("UNUSED_PARAMETER")
|
||||
|
||||
package node.testutils
|
||||
|
||||
import contracts.DummyContract
|
||||
import core.contracts.StateRef
|
||||
import core.crypto.Party
|
||||
import core.testing.DUMMY_NOTARY
|
||||
import core.testing.DUMMY_NOTARY_KEY
|
||||
import node.core.AbstractNode
|
||||
import java.util.*
|
||||
|
||||
fun issueState(node: AbstractNode, notary: Party = DUMMY_NOTARY): StateRef {
|
||||
val tx = DummyContract().generateInitial(node.info.identity.ref(0), Random().nextInt(), DUMMY_NOTARY)
|
||||
tx.signWith(node.storage.myLegalIdentityKey)
|
||||
tx.signWith(DUMMY_NOTARY_KEY)
|
||||
val stx = tx.toSignedTransaction()
|
||||
node.services.recordTransactions(listOf(stx))
|
||||
return StateRef(stx.id, 0)
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package core.testing
|
||||
package node.core.testing
|
||||
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
@ -6,9 +6,10 @@ import contracts.CommercialPaper
|
||||
import core.contracts.DOLLARS
|
||||
import core.contracts.SignedTransaction
|
||||
import core.days
|
||||
import core.node.subsystems.NodeWalletService
|
||||
import core.random63BitValue
|
||||
import core.seconds
|
||||
import node.services.network.InMemoryMessagingNetwork
|
||||
import node.services.wallet.NodeWalletService
|
||||
import protocols.TwoPartyTradeProtocol
|
||||
import java.time.Instant
|
||||
|
@ -1,8 +1,8 @@
|
||||
package core.node.services
|
||||
package node.services.api
|
||||
|
||||
import core.messaging.Message
|
||||
import core.messaging.MessagingService
|
||||
import core.node.subsystems.TOPIC_DEFAULT_POSTFIX
|
||||
import core.node.services.TOPIC_DEFAULT_POSTFIX
|
||||
import core.serialization.deserialize
|
||||
import core.serialization.serialize
|
||||
import protocols.AbstractRequestMessage
|
@ -1,4 +1,4 @@
|
||||
package core.node
|
||||
package node.services.api
|
||||
|
||||
import java.io.InputStream
|
||||
|
@ -1,4 +1,4 @@
|
||||
package core.node.storage
|
||||
package node.services.api
|
||||
|
||||
import core.crypto.sha256
|
||||
import core.protocols.ProtocolStateMachine
|
10
node/src/main/kotlin/node/services/api/MonitoringService.kt
Normal file
10
node/src/main/kotlin/node/services/api/MonitoringService.kt
Normal file
@ -0,0 +1,10 @@
|
||||
package node.services.api
|
||||
|
||||
import com.codahale.metrics.MetricRegistry
|
||||
|
||||
|
||||
/**
|
||||
* Provides access to various metrics and ways to notify monitoring services of things, for sysadmin purposes.
|
||||
* This is not an interface because it is too lightweight to bother mocking out.
|
||||
*/
|
||||
class MonitoringService(val metrics: MetricRegistry)
|
@ -1,4 +1,6 @@
|
||||
package core.node.services
|
||||
package node.services.api
|
||||
|
||||
import core.node.services.ServiceType
|
||||
|
||||
/**
|
||||
* Placeholder interface for regulator services.
|
@ -0,0 +1,7 @@
|
||||
package node.services.api
|
||||
|
||||
import core.node.ServiceHub
|
||||
|
||||
interface ServiceHubInternal : ServiceHub {
|
||||
val monitoringService: MonitoringService
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
package core.node.services
|
||||
package node.services.clientapi
|
||||
|
||||
import core.*
|
||||
import core.contracts.*
|
||||
import core.crypto.DigitalSignature
|
||||
import core.crypto.Party
|
||||
@ -8,12 +7,10 @@ import core.crypto.signWithECDSA
|
||||
import core.math.CubicSplineInterpolator
|
||||
import core.math.Interpolator
|
||||
import core.math.InterpolatorFactory
|
||||
import core.messaging.Message
|
||||
import core.messaging.MessagingService
|
||||
import core.messaging.send
|
||||
import core.node.AbstractNode
|
||||
import core.node.AcceptsFileUpload
|
||||
import core.serialization.deserialize
|
||||
import core.node.services.ServiceType
|
||||
import node.core.AbstractNode
|
||||
import node.services.api.AbstractNodeService
|
||||
import node.services.api.AcceptsFileUpload
|
||||
import org.slf4j.LoggerFactory
|
||||
import protocols.RatesFixProtocol
|
||||
import java.io.InputStream
|
||||
@ -39,9 +36,9 @@ object NodeInterestRates {
|
||||
*/
|
||||
class Service(node: AbstractNode) : AcceptsFileUpload, AbstractNodeService(node.services.networkService) {
|
||||
val ss = node.services.storageService
|
||||
val oracle = Oracle(ss.myLegalIdentity, ss.myLegalIdentityKey)
|
||||
val oracle = NodeInterestRates.Oracle(ss.myLegalIdentity, ss.myLegalIdentityKey)
|
||||
|
||||
private val logger = LoggerFactory.getLogger(NodeInterestRates.Service::class.java)
|
||||
private val logger = LoggerFactory.getLogger(Service::class.java)
|
||||
|
||||
init {
|
||||
addMessageHandler(RatesFixProtocol.TOPIC_SIGN,
|
||||
@ -128,7 +125,7 @@ object NodeInterestRates {
|
||||
}
|
||||
|
||||
/** Fix container, for every fix name & date pair stores a tenor to interest rate map - [InterpolatingRateMap] */
|
||||
class FixContainer(val fixes: List<Fix>, val factory: InterpolatorFactory = CubicSplineInterpolator.Factory) {
|
||||
class FixContainer(val fixes: List<Fix>, val factory: InterpolatorFactory = CubicSplineInterpolator) {
|
||||
private val container = buildContainer(fixes)
|
||||
val size = fixes.size
|
||||
|
@ -1,4 +1,4 @@
|
||||
package core.node
|
||||
package node.services.config
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigFactory
|
@ -1,4 +1,4 @@
|
||||
package core.node.subsystems
|
||||
package node.services.identity
|
||||
|
||||
import core.crypto.Party
|
||||
import core.node.services.IdentityService
|
@ -1,8 +1,8 @@
|
||||
package core.node.subsystems
|
||||
package node.services.keys
|
||||
|
||||
import core.ThreadBox
|
||||
import core.crypto.generateKeyPair
|
||||
import core.node.subsystems.KeyManagementService
|
||||
import core.node.services.KeyManagementService
|
||||
import java.security.KeyPair
|
||||
import java.security.PrivateKey
|
||||
import java.security.PublicKey
|
@ -1,10 +1,10 @@
|
||||
package core.node.subsystems
|
||||
package node.services.messaging
|
||||
|
||||
import com.google.common.net.HostAndPort
|
||||
import core.RunOnCallerThread
|
||||
import core.ThreadBox
|
||||
import core.messaging.*
|
||||
import core.node.Node
|
||||
import node.core.Node
|
||||
import core.utilities.loggerFor
|
||||
import org.apache.activemq.artemis.api.core.SimpleString
|
||||
import org.apache.activemq.artemis.api.core.TransportConfiguration
|
@ -1,4 +1,4 @@
|
||||
package core.testing
|
||||
package node.services.network
|
||||
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
@ -1,4 +1,4 @@
|
||||
package core.node.subsystems
|
||||
package node.services.network
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.MoreExecutors
|
||||
@ -10,11 +10,17 @@ import core.messaging.MessagingService
|
||||
import core.messaging.runOnNextMessage
|
||||
import core.messaging.send
|
||||
import core.node.NodeInfo
|
||||
import core.node.services.*
|
||||
import core.node.services.NetworkCacheError
|
||||
import core.node.services.NetworkMapCache
|
||||
import core.node.services.ServiceType
|
||||
import core.node.services.TOPIC_DEFAULT_POSTFIX
|
||||
import core.random63BitValue
|
||||
import core.serialization.deserialize
|
||||
import core.serialization.serialize
|
||||
import core.utilities.AddOrRemove
|
||||
import node.services.api.RegulatorService
|
||||
import node.services.clientapi.NodeInterestRates
|
||||
import node.services.transactions.NotaryService
|
||||
import node.utilities.AddOrRemove
|
||||
import java.security.PublicKey
|
||||
import java.security.SignatureException
|
||||
import java.util.*
|
@ -5,13 +5,12 @@
|
||||
*
|
||||
* All other rights reserved.
|
||||
*/
|
||||
package core.testing
|
||||
package node.services.network
|
||||
|
||||
import co.paralleluniverse.common.util.VisibleForTesting
|
||||
import core.crypto.Party
|
||||
import core.crypto.DummyPublicKey
|
||||
import core.messaging.SingleMessageRecipient
|
||||
import core.node.subsystems.InMemoryNetworkMapCache
|
||||
import core.node.NodeInfo
|
||||
|
||||
/**
|
@ -1,25 +1,24 @@
|
||||
package core.node.services
|
||||
package node.services.network
|
||||
|
||||
import co.paralleluniverse.common.util.VisibleForTesting
|
||||
import core.crypto.Party
|
||||
import core.ThreadBox
|
||||
import core.crypto.DigitalSignature
|
||||
import core.crypto.SecureHash
|
||||
import core.crypto.SignedData
|
||||
import core.crypto.signWithECDSA
|
||||
import core.crypto.*
|
||||
import core.messaging.MessageRecipients
|
||||
import core.messaging.MessagingService
|
||||
import core.messaging.SingleMessageRecipient
|
||||
import core.node.NodeInfo
|
||||
import core.node.subsystems.NetworkMapCache
|
||||
import core.node.subsystems.TOPIC_DEFAULT_POSTFIX
|
||||
import core.node.services.ServiceType
|
||||
import core.node.services.NetworkMapCache
|
||||
import core.node.services.TOPIC_DEFAULT_POSTFIX
|
||||
import core.serialization.SerializedBytes
|
||||
import core.serialization.deserialize
|
||||
import core.serialization.serialize
|
||||
import core.utilities.AddOrRemove
|
||||
import node.services.api.AbstractNodeService
|
||||
import node.utilities.AddOrRemove
|
||||
import org.slf4j.LoggerFactory
|
||||
import protocols.AbstractRequestMessage
|
||||
import java.security.PrivateKey
|
||||
import java.security.SignatureException
|
||||
import java.time.Instant
|
||||
import java.time.Period
|
||||
import java.util.*
|
||||
@ -200,7 +199,7 @@ class InMemoryNetworkMapService(net: MessagingService, home: NodeRegistration, v
|
||||
|
||||
try {
|
||||
change = req.wireReg.verified()
|
||||
} catch(e: java.security.SignatureException) {
|
||||
} catch(e: SignatureException) {
|
||||
throw NodeMapError.InvalidSignature()
|
||||
}
|
||||
val node = change.node
|
@ -1,8 +1,9 @@
|
||||
package core.node.subsystems
|
||||
package node.services.persistence
|
||||
|
||||
import core.contracts.SignedTransaction
|
||||
import core.messaging.MessagingService
|
||||
import core.node.services.AbstractNodeService
|
||||
import core.node.services.StorageService
|
||||
import node.services.api.AbstractNodeService
|
||||
import core.utilities.loggerFor
|
||||
import protocols.FetchAttachmentsProtocol
|
||||
import protocols.FetchDataProtocol
|
@ -1,4 +1,4 @@
|
||||
package core.node.services
|
||||
package node.services.persistence
|
||||
|
||||
import com.codahale.metrics.MetricRegistry
|
||||
import com.google.common.annotations.VisibleForTesting
|
||||
@ -8,10 +8,12 @@ import com.google.common.io.CountingInputStream
|
||||
import core.contracts.Attachment
|
||||
import core.crypto.SecureHash
|
||||
import core.extractZipFile
|
||||
import core.node.AcceptsFileUpload
|
||||
import node.services.api.AcceptsFileUpload
|
||||
import core.node.services.AttachmentStorage
|
||||
import core.utilities.loggerFor
|
||||
import java.io.FilterInputStream
|
||||
import java.io.InputStream
|
||||
import java.nio.file.FileAlreadyExistsException
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
@ -128,7 +130,7 @@ class NodeAttachmentService(val storePath: Path, val metrics: MetricRegistry) :
|
||||
try {
|
||||
Files.createDirectory(extractTo)
|
||||
extractZipFile(finalPath, extractTo)
|
||||
} catch(e: java.nio.file.FileAlreadyExistsException) {
|
||||
} catch(e: FileAlreadyExistsException) {
|
||||
log.trace("Did not extract attachment jar to directory because it already exists")
|
||||
} catch(e: Exception) {
|
||||
log.error("Failed to extract attachment jar $id, ", e)
|
@ -1,10 +1,12 @@
|
||||
package core.node.storage
|
||||
package node.services.persistence
|
||||
|
||||
import core.serialization.SerializedBytes
|
||||
import core.serialization.deserialize
|
||||
import core.serialization.serialize
|
||||
import core.utilities.loggerFor
|
||||
import core.utilities.trace
|
||||
import node.services.api.Checkpoint
|
||||
import node.services.api.CheckpointStorage
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.StandardCopyOption
|
||||
@ -38,7 +40,7 @@ class PerFileCheckpointStorage(val storeDir: Path) : CheckpointStorage {
|
||||
|
||||
override fun addCheckpoint(checkpoint: Checkpoint) {
|
||||
val serialisedCheckpoint = checkpoint.serialize()
|
||||
val fileName = "${serialisedCheckpoint.hash.toString().toLowerCase()}$fileExtension"
|
||||
val fileName = "${serialisedCheckpoint.hash.toString().toLowerCase()}${fileExtension}"
|
||||
val checkpointFile = storeDir.resolve(fileName)
|
||||
atomicWrite(checkpointFile, serialisedCheckpoint)
|
||||
logger.trace { "Stored $checkpoint to $checkpointFile" }
|
@ -1,10 +1,10 @@
|
||||
package core.node.subsystems
|
||||
package node.services.persistence
|
||||
|
||||
import core.crypto.Party
|
||||
import core.contracts.SignedTransaction
|
||||
import core.crypto.SecureHash
|
||||
import core.node.services.AttachmentStorage
|
||||
import core.node.storage.CheckpointStorage
|
||||
import core.node.services.StorageService
|
||||
import core.utilities.RecordingMap
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.security.KeyPair
|
@ -1,4 +1,4 @@
|
||||
package core.protocols
|
||||
package node.services.statemachine
|
||||
|
||||
import co.paralleluniverse.fibers.Fiber
|
||||
import co.paralleluniverse.fibers.FiberScheduler
|
||||
@ -7,12 +7,15 @@ import co.paralleluniverse.io.serialization.kryo.KryoSerializer
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import core.messaging.MessageRecipients
|
||||
import core.messaging.StateMachineManager
|
||||
import node.services.statemachine.StateMachineManager
|
||||
import core.node.ServiceHub
|
||||
import core.protocols.ProtocolLogic
|
||||
import core.protocols.ProtocolStateMachine
|
||||
import core.serialization.SerializedBytes
|
||||
import core.serialization.createKryo
|
||||
import core.serialization.serialize
|
||||
import core.utilities.UntrustworthyData
|
||||
import node.services.api.ServiceHubInternal
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
@ -29,7 +32,7 @@ class ProtocolStateMachineImpl<R>(val logic: ProtocolLogic<R>, scheduler: FiberS
|
||||
// These fields shouldn't be serialised, so they are marked @Transient.
|
||||
@Transient private var suspendAction: ((result: StateMachineManager.FiberRequest, serialisedFiber: SerializedBytes<ProtocolStateMachineImpl<*>>) -> Unit)? = null
|
||||
@Transient private var resumeWithObject: Any? = null
|
||||
@Transient lateinit override var serviceHub: ServiceHub
|
||||
@Transient lateinit override var serviceHub: ServiceHubInternal
|
||||
|
||||
@Transient private var _logger: Logger? = null
|
||||
override val logger: Logger get() {
|
||||
@ -54,7 +57,7 @@ class ProtocolStateMachineImpl<R>(val logic: ProtocolLogic<R>, scheduler: FiberS
|
||||
logic.psm = this
|
||||
}
|
||||
|
||||
fun prepareForResumeWith(serviceHub: ServiceHub,
|
||||
fun prepareForResumeWith(serviceHub: ServiceHubInternal,
|
||||
withObject: Any?,
|
||||
suspendAction: (StateMachineManager.FiberRequest, SerializedBytes<ProtocolStateMachineImpl<*>>) -> Unit) {
|
||||
this.suspendAction = suspendAction
|
@ -1,4 +1,4 @@
|
||||
package core.messaging
|
||||
package node.services.statemachine
|
||||
|
||||
import co.paralleluniverse.fibers.Fiber
|
||||
import co.paralleluniverse.fibers.FiberExecutorScheduler
|
||||
@ -7,20 +7,22 @@ import com.codahale.metrics.Gauge
|
||||
import com.esotericsoftware.kryo.io.Input
|
||||
import com.google.common.base.Throwables
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import core.node.ServiceHub
|
||||
import core.node.storage.Checkpoint
|
||||
import core.node.storage.CheckpointStorage
|
||||
import core.messaging.MessageRecipients
|
||||
import core.messaging.runOnNextMessage
|
||||
import core.messaging.send
|
||||
import core.protocols.ProtocolLogic
|
||||
import core.protocols.ProtocolStateMachine
|
||||
import core.protocols.ProtocolStateMachineImpl
|
||||
import core.serialization.SerializedBytes
|
||||
import core.serialization.THREAD_LOCAL_KRYO
|
||||
import core.serialization.createKryo
|
||||
import core.serialization.deserialize
|
||||
import core.then
|
||||
import core.utilities.AffinityExecutor
|
||||
import core.utilities.ProgressTracker
|
||||
import core.utilities.trace
|
||||
import node.services.api.Checkpoint
|
||||
import node.services.api.CheckpointStorage
|
||||
import node.services.api.ServiceHubInternal
|
||||
import node.utilities.AffinityExecutor
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
import java.util.*
|
||||
@ -54,7 +56,7 @@ import javax.annotation.concurrent.ThreadSafe
|
||||
* TODO: Implement stub/skel classes that provide a basic RPC framework on top of this.
|
||||
*/
|
||||
@ThreadSafe
|
||||
class StateMachineManager(val serviceHub: ServiceHub, val checkpointStorage: CheckpointStorage, val executor: AffinityExecutor) {
|
||||
class StateMachineManager(val serviceHub: ServiceHubInternal, val checkpointStorage: CheckpointStorage, val executor: AffinityExecutor) {
|
||||
inner class FiberScheduler : FiberExecutorScheduler("Same thread scheduler", executor)
|
||||
|
||||
val scheduler = FiberScheduler()
|
@ -1,9 +1,11 @@
|
||||
package core.node.services
|
||||
package node.services.transactions
|
||||
|
||||
import core.crypto.Party
|
||||
import core.contracts.StateRef
|
||||
import core.ThreadBox
|
||||
import core.contracts.StateRef
|
||||
import core.contracts.WireTransaction
|
||||
import core.crypto.Party
|
||||
import core.node.services.UniquenessException
|
||||
import core.node.services.UniquenessProvider
|
||||
import java.util.*
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
|
@ -1,17 +1,21 @@
|
||||
package core.node.services
|
||||
package node.services.transactions
|
||||
|
||||
import core.crypto.Party
|
||||
import core.contracts.TimestampCommand
|
||||
import core.contracts.WireTransaction
|
||||
import core.crypto.DigitalSignature
|
||||
import core.crypto.Party
|
||||
import core.crypto.SignedData
|
||||
import core.crypto.signWithECDSA
|
||||
import core.messaging.MessagingService
|
||||
import core.node.services.ServiceType
|
||||
import core.node.services.UniquenessException
|
||||
import core.node.services.UniquenessProvider
|
||||
import core.noneOrSingle
|
||||
import core.serialization.SerializedBytes
|
||||
import core.serialization.deserialize
|
||||
import core.serialization.serialize
|
||||
import core.utilities.loggerFor
|
||||
import node.services.api.AbstractNodeService
|
||||
import protocols.NotaryError
|
||||
import protocols.NotaryException
|
||||
import protocols.NotaryProtocol
|
@ -1,4 +1,4 @@
|
||||
package core.node.services
|
||||
package node.services.transactions
|
||||
|
||||
import core.contracts.TimestampCommand
|
||||
import core.seconds
|
@ -1,14 +1,16 @@
|
||||
package core.node.subsystems
|
||||
package node.services.wallet
|
||||
|
||||
import com.codahale.metrics.Gauge
|
||||
import contracts.Cash
|
||||
import core.*
|
||||
import core.ThreadBox
|
||||
import core.contracts.*
|
||||
import core.crypto.Party
|
||||
import core.crypto.SecureHash
|
||||
import core.node.ServiceHub
|
||||
import core.node.services.Wallet
|
||||
import core.node.services.WalletService
|
||||
import core.utilities.loggerFor
|
||||
import core.utilities.trace
|
||||
import node.services.api.ServiceHubInternal
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
@ -19,7 +21,7 @@ import javax.annotation.concurrent.ThreadSafe
|
||||
* states relevant to us into a database and once such a wallet is implemented, this scaffolding can be removed.
|
||||
*/
|
||||
@ThreadSafe
|
||||
class NodeWalletService(private val services: ServiceHub) : WalletService {
|
||||
class NodeWalletService(private val services: ServiceHubInternal) : WalletService {
|
||||
private val log = loggerFor<NodeWalletService>()
|
||||
|
||||
// Variables inside InnerState are protected with a lock by the ThreadBox and aren't in scope unless you're
|
@ -1,10 +1,11 @@
|
||||
package core.node.subsystems
|
||||
package node.services.wallet
|
||||
|
||||
import contracts.Cash
|
||||
import core.contracts.Amount
|
||||
import core.contracts.ContractState
|
||||
import core.contracts.StateAndRef
|
||||
import core.contracts.sumOrThrow
|
||||
import core.node.services.Wallet
|
||||
import java.util.*
|
||||
|
||||
/**
|
@ -1,7 +1,7 @@
|
||||
package core.node.servlets
|
||||
package node.servlets
|
||||
|
||||
import core.crypto.SecureHash
|
||||
import core.node.subsystems.StorageService
|
||||
import core.node.services.StorageService
|
||||
import core.utilities.loggerFor
|
||||
import java.io.FileNotFoundException
|
||||
import javax.servlet.http.HttpServlet
|
@ -1,8 +1,8 @@
|
||||
package api
|
||||
package node.servlets
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import core.node.ServiceHub
|
||||
import core.utilities.JsonSupport
|
||||
import node.utilities.JsonSupport
|
||||
import javax.ws.rs.ext.ContextResolver
|
||||
import javax.ws.rs.ext.Provider
|
||||
|
||||
@ -13,5 +13,5 @@ import javax.ws.rs.ext.Provider
|
||||
@Provider
|
||||
class Config(val services: ServiceHub) : ContextResolver<ObjectMapper> {
|
||||
val defaultObjectMapper = JsonSupport.createDefaultMapper(services.identityService)
|
||||
override fun getContext(type: java.lang.Class<*>) = defaultObjectMapper
|
||||
override fun getContext(type: Class<*>) = defaultObjectMapper
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
package core.node.servlets
|
||||
package node.servlets
|
||||
|
||||
import core.node.AcceptsFileUpload
|
||||
import core.node.Node
|
||||
import node.services.api.AcceptsFileUpload
|
||||
import node.core.Node
|
||||
import core.utilities.loggerFor
|
||||
import org.apache.commons.fileupload.servlet.ServletFileUpload
|
||||
import java.util.*
|
@ -1,4 +1,4 @@
|
||||
package api
|
||||
package node.servlets
|
||||
|
||||
import javax.ws.rs.container.ContainerRequestContext
|
||||
import javax.ws.rs.container.ContainerResponseContext
|
@ -1,5 +1,8 @@
|
||||
package core.utilities
|
||||
package node.utilities
|
||||
|
||||
import core.utilities.BriefLogFormatter
|
||||
import core.utilities.Emoji
|
||||
import core.utilities.ProgressTracker
|
||||
import org.fusesource.jansi.Ansi
|
||||
import org.fusesource.jansi.AnsiConsole
|
||||
import org.fusesource.jansi.AnsiOutputStream
|
@ -1,4 +1,4 @@
|
||||
package core.utilities
|
||||
package node.utilities
|
||||
|
||||
/**
|
||||
* Enum for when adding/removing something, for example adding or removing an entry in a directory.
|
@ -1,6 +1,7 @@
|
||||
package core.utilities
|
||||
package node.utilities
|
||||
|
||||
import com.google.common.util.concurrent.Uninterruptibles
|
||||
import core.utilities.loggerFor
|
||||
import java.util.*
|
||||
import java.util.concurrent.*
|
||||
import java.util.function.Supplier
|
@ -1,4 +1,4 @@
|
||||
package core.utilities
|
||||
package node.utilities
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.core.JsonParseException
|
@ -1,458 +0,0 @@
|
||||
import contracts.Cash
|
||||
import contracts.DummyContract
|
||||
import contracts.InsufficientBalanceException
|
||||
import core.*
|
||||
import core.contracts.*
|
||||
import core.crypto.Party
|
||||
import core.crypto.SecureHash
|
||||
import core.serialization.OpaqueBytes
|
||||
import core.testutils.*
|
||||
import org.junit.Test
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertNotEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class CashTests {
|
||||
val inState = Cash.State(
|
||||
deposit = MEGA_CORP.ref(1),
|
||||
amount = 1000.DOLLARS,
|
||||
owner = DUMMY_PUBKEY_1,
|
||||
notary = DUMMY_NOTARY
|
||||
)
|
||||
val outState = inState.copy(owner = DUMMY_PUBKEY_2)
|
||||
|
||||
fun Cash.State.editDepositRef(ref: Byte) = copy(deposit = deposit.copy(reference = OpaqueBytes.of(ref)))
|
||||
|
||||
@Test
|
||||
fun trivial() {
|
||||
transaction {
|
||||
input { inState }
|
||||
this `fails requirement` "the amounts balance"
|
||||
|
||||
tweak {
|
||||
output { outState.copy(amount = 2000.DOLLARS) }
|
||||
this `fails requirement` "the amounts balance"
|
||||
}
|
||||
tweak {
|
||||
output { outState }
|
||||
// No command arguments
|
||||
this `fails requirement` "required contracts.Cash.Commands.Move command"
|
||||
}
|
||||
tweak {
|
||||
output { outState }
|
||||
arg(DUMMY_PUBKEY_2) { Cash.Commands.Move() }
|
||||
this `fails requirement` "the owning keys are the same as the signing keys"
|
||||
}
|
||||
tweak {
|
||||
output { outState }
|
||||
output { outState `issued by` MINI_CORP }
|
||||
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
this `fails requirement` "at least one cash input"
|
||||
}
|
||||
// Simple reallocation works.
|
||||
tweak {
|
||||
output { outState }
|
||||
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
this.accepts()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun issueMoney() {
|
||||
// Check we can't "move" money into existence.
|
||||
transaction {
|
||||
input { DummyContract.State(notary = DUMMY_NOTARY) }
|
||||
output { outState }
|
||||
arg(MINI_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
|
||||
this `fails requirement` "there is at least one cash input"
|
||||
}
|
||||
|
||||
// Check we can issue money only as long as the issuer institution is a command signer, i.e. any recognised
|
||||
// institution is allowed to issue as much cash as they want.
|
||||
transaction {
|
||||
output { outState }
|
||||
arg(DUMMY_PUBKEY_1) { Cash.Commands.Issue() }
|
||||
this `fails requirement` "output deposits are owned by a command signer"
|
||||
}
|
||||
transaction {
|
||||
output {
|
||||
Cash.State(
|
||||
amount = 1000.DOLLARS,
|
||||
owner = DUMMY_PUBKEY_1,
|
||||
deposit = MINI_CORP.ref(12, 34),
|
||||
notary = DUMMY_NOTARY
|
||||
)
|
||||
}
|
||||
tweak {
|
||||
arg(MINI_CORP_PUBKEY) { Cash.Commands.Issue(0) }
|
||||
this `fails requirement` "has a nonce"
|
||||
}
|
||||
arg(MINI_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||
this.accepts()
|
||||
}
|
||||
|
||||
// Test generation works.
|
||||
val ptx = TransactionBuilder()
|
||||
Cash().generateIssue(ptx, 100.DOLLARS, MINI_CORP.ref(12, 34), owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY)
|
||||
assertTrue(ptx.inputStates().isEmpty())
|
||||
val s = ptx.outputStates()[0] as Cash.State
|
||||
assertEquals(100.DOLLARS, s.amount)
|
||||
assertEquals(MINI_CORP, s.deposit.party)
|
||||
assertEquals(DUMMY_PUBKEY_1, s.owner)
|
||||
assertTrue(ptx.commands()[0].value is Cash.Commands.Issue)
|
||||
assertEquals(MINI_CORP_PUBKEY, ptx.commands()[0].signers[0])
|
||||
|
||||
// Test issuance from the issuance definition
|
||||
val issuanceDef = Cash.IssuanceDefinition(MINI_CORP.ref(12, 34), USD)
|
||||
val templatePtx = TransactionBuilder()
|
||||
Cash().generateIssue(templatePtx, issuanceDef, 100.DOLLARS.pennies, owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY)
|
||||
assertTrue(templatePtx.inputStates().isEmpty())
|
||||
assertEquals(ptx.outputStates()[0], templatePtx.outputStates()[0])
|
||||
|
||||
// We can consume $1000 in a transaction and output $2000 as long as it's signed by an issuer.
|
||||
transaction {
|
||||
input { inState }
|
||||
output { inState.copy(amount = inState.amount * 2) }
|
||||
|
||||
// Move fails: not allowed to summon money.
|
||||
tweak {
|
||||
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
this `fails requirement` "at issuer MegaCorp the amounts balance"
|
||||
}
|
||||
|
||||
// Issue works.
|
||||
tweak {
|
||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||
this.accepts()
|
||||
}
|
||||
}
|
||||
|
||||
// Can't use an issue command to lower the amount.
|
||||
transaction {
|
||||
input { inState }
|
||||
output { inState.copy(amount = inState.amount / 2) }
|
||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||
this `fails requirement` "output values sum to more than the inputs"
|
||||
}
|
||||
|
||||
// Can't have an issue command that doesn't actually issue money.
|
||||
transaction {
|
||||
input { inState }
|
||||
output { inState }
|
||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||
this `fails requirement` "output values sum to more than the inputs"
|
||||
}
|
||||
|
||||
// Can't have any other commands if we have an issue command (because the issue command overrules them)
|
||||
transaction {
|
||||
input { inState }
|
||||
output { inState.copy(amount = inState.amount * 2) }
|
||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||
tweak {
|
||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||
this `fails requirement` "there is only a single issue command"
|
||||
}
|
||||
tweak {
|
||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
this `fails requirement` "there is only a single issue command"
|
||||
}
|
||||
tweak {
|
||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(inState.amount / 2) }
|
||||
this `fails requirement` "there is only a single issue command"
|
||||
}
|
||||
this.accepts()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMergeSplit() {
|
||||
// Splitting value works.
|
||||
transaction {
|
||||
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
tweak {
|
||||
input { inState }
|
||||
for (i in 1..4) output { inState.copy(amount = inState.amount / 4) }
|
||||
this.accepts()
|
||||
}
|
||||
// Merging 4 inputs into 2 outputs works.
|
||||
tweak {
|
||||
for (i in 1..4) input { inState.copy(amount = inState.amount / 4) }
|
||||
output { inState.copy(amount = inState.amount / 2) }
|
||||
output { inState.copy(amount = inState.amount / 2) }
|
||||
this.accepts()
|
||||
}
|
||||
// Merging 2 inputs into 1 works.
|
||||
tweak {
|
||||
input { inState.copy(amount = inState.amount / 2) }
|
||||
input { inState.copy(amount = inState.amount / 2) }
|
||||
output { inState }
|
||||
this.accepts()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun zeroSizedValues() {
|
||||
transaction {
|
||||
input { inState }
|
||||
input { inState.copy(amount = 0.DOLLARS) }
|
||||
this `fails requirement` "zero sized inputs"
|
||||
}
|
||||
transaction {
|
||||
input { inState }
|
||||
output { inState }
|
||||
output { inState.copy(amount = 0.DOLLARS) }
|
||||
this `fails requirement` "zero sized outputs"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun trivialMismatches() {
|
||||
// Can't change issuer.
|
||||
transaction {
|
||||
input { inState }
|
||||
output { outState `issued by` MINI_CORP }
|
||||
this `fails requirement` "at issuer MegaCorp the amounts balance"
|
||||
}
|
||||
// Can't change deposit reference when splitting.
|
||||
transaction {
|
||||
input { inState }
|
||||
output { outState.editDepositRef(0).copy(amount = inState.amount / 2) }
|
||||
output { outState.editDepositRef(1).copy(amount = inState.amount / 2) }
|
||||
this `fails requirement` "for deposit [01] at issuer MegaCorp the amounts balance"
|
||||
}
|
||||
// Can't mix currencies.
|
||||
transaction {
|
||||
input { inState }
|
||||
output { outState.copy(amount = 800.DOLLARS) }
|
||||
output { outState.copy(amount = 200.POUNDS) }
|
||||
this `fails requirement` "the amounts balance"
|
||||
}
|
||||
transaction {
|
||||
input { inState }
|
||||
input {
|
||||
inState.copy(
|
||||
amount = 150.POUNDS,
|
||||
owner = DUMMY_PUBKEY_2
|
||||
)
|
||||
}
|
||||
output { outState.copy(amount = 1150.DOLLARS) }
|
||||
this `fails requirement` "the amounts balance"
|
||||
}
|
||||
// Can't have superfluous input states from different issuers.
|
||||
transaction {
|
||||
input { inState }
|
||||
input { inState `issued by` MINI_CORP }
|
||||
output { outState }
|
||||
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
this `fails requirement` "at issuer MiniCorp the amounts balance"
|
||||
}
|
||||
// Can't combine two different deposits at the same issuer.
|
||||
transaction {
|
||||
input { inState }
|
||||
input { inState.editDepositRef(3) }
|
||||
output { outState.copy(amount = inState.amount * 2).editDepositRef(3) }
|
||||
this `fails requirement` "for deposit [01]"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun exitLedger() {
|
||||
// Single input/output straightforward case.
|
||||
transaction {
|
||||
input { inState }
|
||||
output { outState.copy(amount = inState.amount - 200.DOLLARS) }
|
||||
|
||||
tweak {
|
||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(100.DOLLARS) }
|
||||
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
this `fails requirement` "the amounts balance"
|
||||
}
|
||||
|
||||
tweak {
|
||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS) }
|
||||
this `fails requirement` "required contracts.Cash.Commands.Move command"
|
||||
|
||||
tweak {
|
||||
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
this.accepts()
|
||||
}
|
||||
}
|
||||
}
|
||||
// Multi-issuer case.
|
||||
transaction {
|
||||
input { inState }
|
||||
input { inState `issued by` MINI_CORP }
|
||||
|
||||
output { inState.copy(amount = inState.amount - 200.DOLLARS) `issued by` MINI_CORP }
|
||||
output { inState.copy(amount = inState.amount - 200.DOLLARS) }
|
||||
|
||||
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
|
||||
this `fails requirement` "at issuer MegaCorp the amounts balance"
|
||||
|
||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS) }
|
||||
this `fails requirement` "at issuer MiniCorp the amounts balance"
|
||||
|
||||
arg(MINI_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS) }
|
||||
this.accepts()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun multiIssuer() {
|
||||
transaction {
|
||||
// Gather 2000 dollars from two different issuers.
|
||||
input { inState }
|
||||
input { inState `issued by` MINI_CORP }
|
||||
|
||||
// Can't merge them together.
|
||||
tweak {
|
||||
output { inState.copy(owner = DUMMY_PUBKEY_2, amount = 2000.DOLLARS) }
|
||||
this `fails requirement` "at issuer MegaCorp the amounts balance"
|
||||
}
|
||||
// Missing MiniCorp deposit
|
||||
tweak {
|
||||
output { inState.copy(owner = DUMMY_PUBKEY_2) }
|
||||
output { inState.copy(owner = DUMMY_PUBKEY_2) }
|
||||
this `fails requirement` "at issuer MegaCorp the amounts balance"
|
||||
}
|
||||
|
||||
// This works.
|
||||
output { inState.copy(owner = DUMMY_PUBKEY_2) }
|
||||
output { inState.copy(owner = DUMMY_PUBKEY_2) `issued by` MINI_CORP }
|
||||
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
this.accepts()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun multiCurrency() {
|
||||
// Check we can do an atomic currency trade tx.
|
||||
transaction {
|
||||
val pounds = Cash.State(MINI_CORP.ref(3, 4, 5), 658.POUNDS, DUMMY_PUBKEY_2, DUMMY_NOTARY)
|
||||
input { inState `owned by` DUMMY_PUBKEY_1 }
|
||||
input { pounds }
|
||||
output { inState `owned by` DUMMY_PUBKEY_2 }
|
||||
output { pounds `owned by` DUMMY_PUBKEY_1 }
|
||||
arg(DUMMY_PUBKEY_1, DUMMY_PUBKEY_2) { Cash.Commands.Move() }
|
||||
|
||||
this.accepts()
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Spend tx generation
|
||||
|
||||
val OUR_PUBKEY_1 = DUMMY_PUBKEY_1
|
||||
val THEIR_PUBKEY_1 = DUMMY_PUBKEY_2
|
||||
|
||||
fun makeCash(amount: Amount, corp: Party, depositRef: Byte = 1) =
|
||||
StateAndRef(
|
||||
Cash.State(corp.ref(depositRef), amount, OUR_PUBKEY_1, DUMMY_NOTARY),
|
||||
StateRef(SecureHash.randomSHA256(), Random().nextInt(32))
|
||||
)
|
||||
|
||||
val WALLET = listOf(
|
||||
makeCash(100.DOLLARS, MEGA_CORP),
|
||||
makeCash(400.DOLLARS, MEGA_CORP),
|
||||
makeCash(80.DOLLARS, MINI_CORP),
|
||||
makeCash(80.SWISS_FRANCS, MINI_CORP, 2)
|
||||
)
|
||||
|
||||
fun makeSpend(amount: Amount, dest: PublicKey): WireTransaction {
|
||||
val tx = TransactionBuilder()
|
||||
Cash().generateSpend(tx, amount, dest, WALLET)
|
||||
return tx.toWireTransaction()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun generateSimpleDirectSpend() {
|
||||
val wtx = makeSpend(100.DOLLARS, THEIR_PUBKEY_1)
|
||||
assertEquals(WALLET[0].ref, wtx.inputs[0])
|
||||
assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1), wtx.outputs[0])
|
||||
assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun generateSimpleSpendWithParties() {
|
||||
val tx = TransactionBuilder()
|
||||
Cash().generateSpend(tx, 80.DOLLARS, ALICE_PUBKEY, WALLET, setOf(MINI_CORP))
|
||||
assertEquals(WALLET[2].ref, tx.inputStates()[0])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun generateSimpleSpendWithChange() {
|
||||
val wtx = makeSpend(10.DOLLARS, THEIR_PUBKEY_1)
|
||||
assertEquals(WALLET[0].ref, wtx.inputs[0])
|
||||
assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1, amount = 10.DOLLARS), wtx.outputs[0])
|
||||
assertEquals(WALLET[0].state.copy(amount = 90.DOLLARS), wtx.outputs[1])
|
||||
assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun generateSpendWithTwoInputs() {
|
||||
val wtx = makeSpend(500.DOLLARS, THEIR_PUBKEY_1)
|
||||
assertEquals(WALLET[0].ref, wtx.inputs[0])
|
||||
assertEquals(WALLET[1].ref, wtx.inputs[1])
|
||||
assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS), wtx.outputs[0])
|
||||
assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun generateSpendMixedDeposits() {
|
||||
val wtx = makeSpend(580.DOLLARS, THEIR_PUBKEY_1)
|
||||
assertEquals(WALLET[0].ref, wtx.inputs[0])
|
||||
assertEquals(WALLET[1].ref, wtx.inputs[1])
|
||||
assertEquals(WALLET[2].ref, wtx.inputs[2])
|
||||
assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS), wtx.outputs[0])
|
||||
assertEquals(WALLET[2].state.copy(owner = THEIR_PUBKEY_1), wtx.outputs[1])
|
||||
assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun generateSpendInsufficientBalance() {
|
||||
val e: InsufficientBalanceException = assertFailsWith("balance") {
|
||||
makeSpend(1000.DOLLARS, THEIR_PUBKEY_1)
|
||||
}
|
||||
assertEquals((1000 - 580).DOLLARS, e.amountMissing)
|
||||
|
||||
assertFailsWith(InsufficientBalanceException::class) {
|
||||
makeSpend(81.SWISS_FRANCS, THEIR_PUBKEY_1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm that aggregation of states is correctly modelled.
|
||||
*/
|
||||
@Test
|
||||
fun aggregation() {
|
||||
val fiveThousandDollarsFromMega = Cash.State(MEGA_CORP.ref(2), 5000.DOLLARS, MEGA_CORP_PUBKEY, DUMMY_NOTARY)
|
||||
val twoThousandDollarsFromMega = Cash.State(MEGA_CORP.ref(2), 2000.DOLLARS, MINI_CORP_PUBKEY, DUMMY_NOTARY)
|
||||
val oneThousandDollarsFromMini = Cash.State(MINI_CORP.ref(3), 1000.DOLLARS, MEGA_CORP_PUBKEY, DUMMY_NOTARY)
|
||||
|
||||
// Obviously it must be possible to aggregate states with themselves
|
||||
assertEquals(fiveThousandDollarsFromMega.issuanceDef, fiveThousandDollarsFromMega.issuanceDef)
|
||||
|
||||
// Owner is not considered when calculating whether it is possible to aggregate states
|
||||
assertEquals(fiveThousandDollarsFromMega.issuanceDef, twoThousandDollarsFromMega.issuanceDef)
|
||||
|
||||
// States cannot be aggregated if the deposit differs
|
||||
assertNotEquals(fiveThousandDollarsFromMega.issuanceDef, oneThousandDollarsFromMini.issuanceDef)
|
||||
assertNotEquals(twoThousandDollarsFromMega.issuanceDef, oneThousandDollarsFromMini.issuanceDef)
|
||||
|
||||
// States cannot be aggregated if the currency differs
|
||||
assertNotEquals(oneThousandDollarsFromMini.issuanceDef,
|
||||
Cash.State(MINI_CORP.ref(3), 1000.POUNDS, MEGA_CORP_PUBKEY, DUMMY_NOTARY).issuanceDef)
|
||||
|
||||
// States cannot be aggregated if the reference differs
|
||||
assertNotEquals(fiveThousandDollarsFromMega.issuanceDef, fiveThousandDollarsFromMega.copy(deposit = MEGA_CORP.ref(1)).issuanceDef)
|
||||
assertNotEquals(fiveThousandDollarsFromMega.copy(deposit = MEGA_CORP.ref(1)).issuanceDef, fiveThousandDollarsFromMega.issuanceDef)
|
||||
}
|
||||
}
|
@ -1,246 +0,0 @@
|
||||
package contracts
|
||||
|
||||
import core.*
|
||||
import core.contracts.*
|
||||
import core.crypto.SecureHash
|
||||
import core.testutils.*
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
import java.time.Instant
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
interface ICommercialPaperTestTemplate {
|
||||
open fun getPaper(): ICommercialPaperState
|
||||
open fun getIssueCommand(): CommandData
|
||||
open fun getRedeemCommand(): CommandData
|
||||
open fun getMoveCommand(): CommandData
|
||||
}
|
||||
|
||||
class JavaCommercialPaperTest() : ICommercialPaperTestTemplate {
|
||||
override fun getPaper(): ICommercialPaperState = JavaCommercialPaper.State(
|
||||
MEGA_CORP.ref(123),
|
||||
MEGA_CORP_PUBKEY,
|
||||
1000.DOLLARS,
|
||||
TEST_TX_TIME + 7.days,
|
||||
DUMMY_NOTARY
|
||||
)
|
||||
|
||||
override fun getIssueCommand(): CommandData = JavaCommercialPaper.Commands.Issue()
|
||||
override fun getRedeemCommand(): CommandData = JavaCommercialPaper.Commands.Redeem()
|
||||
override fun getMoveCommand(): CommandData = JavaCommercialPaper.Commands.Move()
|
||||
}
|
||||
|
||||
class KotlinCommercialPaperTest() : ICommercialPaperTestTemplate {
|
||||
override fun getPaper(): ICommercialPaperState = CommercialPaper.State(
|
||||
issuance = MEGA_CORP.ref(123),
|
||||
owner = MEGA_CORP_PUBKEY,
|
||||
faceValue = 1000.DOLLARS,
|
||||
maturityDate = TEST_TX_TIME + 7.days,
|
||||
notary = DUMMY_NOTARY
|
||||
)
|
||||
|
||||
override fun getIssueCommand(): CommandData = CommercialPaper.Commands.Issue()
|
||||
override fun getRedeemCommand(): CommandData = CommercialPaper.Commands.Redeem()
|
||||
override fun getMoveCommand(): CommandData = CommercialPaper.Commands.Move()
|
||||
}
|
||||
|
||||
@RunWith(Parameterized::class)
|
||||
class CommercialPaperTestsGeneric {
|
||||
companion object {
|
||||
@Parameterized.Parameters @JvmStatic
|
||||
fun data() = listOf(JavaCommercialPaperTest(), KotlinCommercialPaperTest())
|
||||
}
|
||||
|
||||
@Parameterized.Parameter
|
||||
lateinit var thisTest: ICommercialPaperTestTemplate
|
||||
|
||||
val attachments = MockStorageService().attachments
|
||||
|
||||
@Test
|
||||
fun ok() {
|
||||
trade().verify()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `not matured at redemption`() {
|
||||
trade(redemptionTime = TEST_TX_TIME + 2.days).expectFailureOfTx(3, "must have matured")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `key mismatch at issue`() {
|
||||
transactionGroup {
|
||||
transaction {
|
||||
output { thisTest.getPaper() }
|
||||
arg(DUMMY_PUBKEY_1) { thisTest.getIssueCommand() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
expectFailureOfTx(1, "signed by the claimed issuer")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `face value is not zero`() {
|
||||
transactionGroup {
|
||||
transaction {
|
||||
output { thisTest.getPaper().withFaceValue(0.DOLLARS) }
|
||||
arg(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
expectFailureOfTx(1, "face value is not zero")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `maturity date not in the past`() {
|
||||
transactionGroup {
|
||||
transaction {
|
||||
output { thisTest.getPaper().withMaturityDate(TEST_TX_TIME - 10.days) }
|
||||
arg(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
expectFailureOfTx(1, "maturity date is not in the past")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `issue cannot replace an existing state`() {
|
||||
transactionGroup {
|
||||
roots {
|
||||
transaction(thisTest.getPaper() label "paper")
|
||||
}
|
||||
transaction {
|
||||
input("paper")
|
||||
output { thisTest.getPaper() }
|
||||
arg(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
expectFailureOfTx(1, "there is no input state")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `did not receive enough money at redemption`() {
|
||||
trade(aliceGetsBack = 700.DOLLARS).expectFailureOfTx(3, "received amount equals the face value")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `paper must be destroyed by redemption`() {
|
||||
trade(destroyPaperAtRedemption = false).expectFailureOfTx(3, "must be destroyed")
|
||||
}
|
||||
|
||||
fun cashOutputsToWallet(vararg states: Cash.State): Pair<LedgerTransaction, List<StateAndRef<Cash.State>>> {
|
||||
val ltx = LedgerTransaction(emptyList(), emptyList(), listOf(*states), emptyList(), SecureHash.randomSHA256())
|
||||
return Pair(ltx, states.mapIndexed { index, state -> StateAndRef(state, StateRef(ltx.id, index)) })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `issue move and then redeem`() {
|
||||
// MiniCorp issues $10,000 of commercial paper, to mature in 30 days, owned initially by itself.
|
||||
val issueTX: LedgerTransaction = run {
|
||||
val ptx = CommercialPaper().generateIssue(MINI_CORP.ref(123), 10000.DOLLARS, TEST_TX_TIME + 30.days, DUMMY_NOTARY).apply {
|
||||
setTime(TEST_TX_TIME, DUMMY_NOTARY, 30.seconds)
|
||||
signWith(MINI_CORP_KEY)
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
}
|
||||
val stx = ptx.toSignedTransaction()
|
||||
stx.verifyToLedgerTransaction(MockIdentityService, attachments)
|
||||
}
|
||||
|
||||
val (alicesWalletTX, alicesWallet) = cashOutputsToWallet(
|
||||
3000.DOLLARS.CASH `owned by` ALICE_PUBKEY,
|
||||
3000.DOLLARS.CASH `owned by` ALICE_PUBKEY,
|
||||
3000.DOLLARS.CASH `owned by` ALICE_PUBKEY
|
||||
)
|
||||
|
||||
// Alice pays $9000 to MiniCorp to own some of their debt.
|
||||
val moveTX: LedgerTransaction = run {
|
||||
val ptx = TransactionBuilder()
|
||||
Cash().generateSpend(ptx, 9000.DOLLARS, MINI_CORP_PUBKEY, alicesWallet)
|
||||
CommercialPaper().generateMove(ptx, issueTX.outRef(0), ALICE_PUBKEY)
|
||||
ptx.signWith(MINI_CORP_KEY)
|
||||
ptx.signWith(ALICE_KEY)
|
||||
ptx.signWith(DUMMY_NOTARY_KEY)
|
||||
ptx.toSignedTransaction().verifyToLedgerTransaction(MockIdentityService, attachments)
|
||||
}
|
||||
|
||||
// Won't be validated.
|
||||
val (corpWalletTX, corpWallet) = cashOutputsToWallet(
|
||||
9000.DOLLARS.CASH `owned by` MINI_CORP_PUBKEY,
|
||||
4000.DOLLARS.CASH `owned by` MINI_CORP_PUBKEY
|
||||
)
|
||||
|
||||
fun makeRedeemTX(time: Instant): LedgerTransaction {
|
||||
val ptx = TransactionBuilder()
|
||||
ptx.setTime(time, DUMMY_NOTARY, 30.seconds)
|
||||
CommercialPaper().generateRedeem(ptx, moveTX.outRef(1), corpWallet)
|
||||
ptx.signWith(ALICE_KEY)
|
||||
ptx.signWith(MINI_CORP_KEY)
|
||||
ptx.signWith(DUMMY_NOTARY_KEY)
|
||||
return ptx.toSignedTransaction().verifyToLedgerTransaction(MockIdentityService, attachments)
|
||||
}
|
||||
|
||||
val tooEarlyRedemption = makeRedeemTX(TEST_TX_TIME + 10.days)
|
||||
val validRedemption = makeRedeemTX(TEST_TX_TIME + 31.days)
|
||||
|
||||
val e = assertFailsWith(TransactionVerificationException::class) {
|
||||
TransactionGroup(setOf(issueTX, moveTX, tooEarlyRedemption), setOf(corpWalletTX, alicesWalletTX)).verify()
|
||||
}
|
||||
assertTrue(e.cause!!.message!!.contains("paper must have matured"))
|
||||
|
||||
TransactionGroup(setOf(issueTX, moveTX, validRedemption), setOf(corpWalletTX, alicesWalletTX)).verify()
|
||||
}
|
||||
|
||||
// Generate a trade lifecycle with various parameters.
|
||||
fun trade(redemptionTime: Instant = TEST_TX_TIME + 8.days,
|
||||
aliceGetsBack: Amount = 1000.DOLLARS,
|
||||
destroyPaperAtRedemption: Boolean = true): TransactionGroupDSL<ICommercialPaperState> {
|
||||
val someProfits = 1200.DOLLARS
|
||||
return transactionGroupFor() {
|
||||
roots {
|
||||
transaction(900.DOLLARS.CASH `owned by` ALICE_PUBKEY label "alice's $900")
|
||||
transaction(someProfits.CASH `owned by` MEGA_CORP_PUBKEY label "some profits")
|
||||
}
|
||||
|
||||
// Some CP is issued onto the ledger by MegaCorp.
|
||||
transaction("Issuance") {
|
||||
output("paper") { thisTest.getPaper() }
|
||||
arg(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
// The CP is sold to alice for her $900, $100 less than the face value. At 10% interest after only 7 days,
|
||||
// that sounds a bit too good to be true!
|
||||
transaction("Trade") {
|
||||
input("paper")
|
||||
input("alice's $900")
|
||||
output("borrowed $900") { 900.DOLLARS.CASH `owned by` MEGA_CORP_PUBKEY }
|
||||
output("alice's paper") { "paper".output `owned by` ALICE_PUBKEY }
|
||||
arg(ALICE_PUBKEY) { Cash.Commands.Move() }
|
||||
arg(MEGA_CORP_PUBKEY) { thisTest.getMoveCommand() }
|
||||
}
|
||||
|
||||
// Time passes, and Alice redeem's her CP for $1000, netting a $100 profit. MegaCorp has received $1200
|
||||
// as a single payment from somewhere and uses it to pay Alice off, keeping the remaining $200 as change.
|
||||
transaction("Redemption") {
|
||||
input("alice's paper")
|
||||
input("some profits")
|
||||
|
||||
output("Alice's profit") { aliceGetsBack.CASH `owned by` ALICE_PUBKEY }
|
||||
output("Change") { (someProfits - aliceGetsBack).CASH `owned by` MEGA_CORP_PUBKEY }
|
||||
if (!destroyPaperAtRedemption)
|
||||
output { "paper".output }
|
||||
|
||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
arg(ALICE_PUBKEY) { thisTest.getRedeemCommand() }
|
||||
|
||||
timestamp(redemptionTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,160 +0,0 @@
|
||||
package contracts
|
||||
|
||||
import core.*
|
||||
import core.contracts.*
|
||||
import core.crypto.SecureHash
|
||||
import core.testutils.*
|
||||
import org.junit.Test
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class CrowdFundTests {
|
||||
val CF_1 = CrowdFund.State(
|
||||
campaign = CrowdFund.Campaign(
|
||||
owner = MINI_CORP_PUBKEY,
|
||||
name = "kickstart me",
|
||||
target = 1000.DOLLARS,
|
||||
closingTime = TEST_TX_TIME + 7.days
|
||||
),
|
||||
closed = false,
|
||||
pledges = ArrayList<CrowdFund.Pledge>(),
|
||||
notary = DUMMY_NOTARY
|
||||
)
|
||||
|
||||
val attachments = MockStorageService().attachments
|
||||
|
||||
@Test
|
||||
fun `key mismatch at issue`() {
|
||||
transactionGroup {
|
||||
transaction {
|
||||
output { CF_1 }
|
||||
arg(DUMMY_PUBKEY_1) { CrowdFund.Commands.Register() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
expectFailureOfTx(1, "the transaction is signed by the owner of the crowdsourcing")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `closing time not in the future`() {
|
||||
transactionGroup {
|
||||
transaction {
|
||||
output { CF_1.copy(campaign = CF_1.campaign.copy(closingTime = TEST_TX_TIME - 1.days)) }
|
||||
arg(MINI_CORP_PUBKEY) { CrowdFund.Commands.Register() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
expectFailureOfTx(1, "the output registration has a closing time in the future")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ok() {
|
||||
raiseFunds().verify()
|
||||
}
|
||||
|
||||
private fun raiseFunds(): TransactionGroupDSL<CrowdFund.State> {
|
||||
return transactionGroupFor {
|
||||
roots {
|
||||
transaction(1000.DOLLARS.CASH `owned by` ALICE_PUBKEY label "alice's $1000")
|
||||
}
|
||||
|
||||
// 1. Create the funding opportunity
|
||||
transaction {
|
||||
output("funding opportunity") { CF_1 }
|
||||
arg(MINI_CORP_PUBKEY) { CrowdFund.Commands.Register() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
// 2. Place a pledge
|
||||
transaction {
|
||||
input ("funding opportunity")
|
||||
input("alice's $1000")
|
||||
output ("pledged opportunity") {
|
||||
CF_1.copy(
|
||||
pledges = CF_1.pledges + CrowdFund.Pledge(ALICE_PUBKEY, 1000.DOLLARS)
|
||||
)
|
||||
}
|
||||
output { 1000.DOLLARS.CASH `owned by` MINI_CORP_PUBKEY }
|
||||
arg(ALICE_PUBKEY) { Cash.Commands.Move() }
|
||||
arg(ALICE_PUBKEY) { CrowdFund.Commands.Pledge() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
// 3. Close the opportunity, assuming the target has been met
|
||||
transaction {
|
||||
input ("pledged opportunity")
|
||||
output ("funded and closed") { "pledged opportunity".output.copy(closed = true) }
|
||||
arg(MINI_CORP_PUBKEY) { CrowdFund.Commands.Close() }
|
||||
timestamp(time = TEST_TX_TIME + 8.days)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cashOutputsToWallet(vararg states: Cash.State): Pair<LedgerTransaction, List<StateAndRef<Cash.State>>> {
|
||||
val ltx = LedgerTransaction(emptyList(), emptyList(), listOf(*states), emptyList(), SecureHash.randomSHA256())
|
||||
return Pair(ltx, states.mapIndexed { index, state -> StateAndRef(state, StateRef(ltx.id, index)) })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `raise more funds using output-state generation functions`() {
|
||||
// MiniCorp registers a crowdfunding of $1,000, to close in 7 days.
|
||||
val registerTX: LedgerTransaction = run {
|
||||
// craftRegister returns a partial transaction
|
||||
val ptx = CrowdFund().generateRegister(MINI_CORP.ref(123), 1000.DOLLARS, "crowd funding", TEST_TX_TIME + 7.days, DUMMY_NOTARY).apply {
|
||||
setTime(TEST_TX_TIME, DUMMY_NOTARY, 30.seconds)
|
||||
signWith(MINI_CORP_KEY)
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
}
|
||||
ptx.toSignedTransaction().verifyToLedgerTransaction(MockIdentityService, attachments)
|
||||
}
|
||||
|
||||
// let's give Alice some funds that she can invest
|
||||
val (aliceWalletTX, aliceWallet) = cashOutputsToWallet(
|
||||
200.DOLLARS.CASH `owned by` ALICE_PUBKEY,
|
||||
500.DOLLARS.CASH `owned by` ALICE_PUBKEY,
|
||||
300.DOLLARS.CASH `owned by` ALICE_PUBKEY
|
||||
)
|
||||
|
||||
// Alice pays $1000 to MiniCorp to fund their campaign.
|
||||
val pledgeTX: LedgerTransaction = run {
|
||||
val ptx = TransactionBuilder()
|
||||
CrowdFund().generatePledge(ptx, registerTX.outRef(0), ALICE_PUBKEY)
|
||||
Cash().generateSpend(ptx, 1000.DOLLARS, MINI_CORP_PUBKEY, aliceWallet)
|
||||
ptx.setTime(TEST_TX_TIME, DUMMY_NOTARY, 30.seconds)
|
||||
ptx.signWith(ALICE_KEY)
|
||||
ptx.signWith(DUMMY_NOTARY_KEY)
|
||||
// this verify passes - the transaction contains an output cash, necessary to verify the fund command
|
||||
ptx.toSignedTransaction().verifyToLedgerTransaction(MockIdentityService, attachments)
|
||||
}
|
||||
|
||||
// Won't be validated.
|
||||
val (miniCorpWalletTx, miniCorpWallet) = cashOutputsToWallet(
|
||||
900.DOLLARS.CASH `owned by` MINI_CORP_PUBKEY,
|
||||
400.DOLLARS.CASH `owned by` MINI_CORP_PUBKEY
|
||||
)
|
||||
// MiniCorp closes their campaign.
|
||||
fun makeFundedTX(time: Instant): LedgerTransaction {
|
||||
val ptx = TransactionBuilder()
|
||||
ptx.setTime(time, DUMMY_NOTARY, 30.seconds)
|
||||
CrowdFund().generateClose(ptx, pledgeTX.outRef(0), miniCorpWallet)
|
||||
ptx.signWith(MINI_CORP_KEY)
|
||||
ptx.signWith(DUMMY_NOTARY_KEY)
|
||||
return ptx.toSignedTransaction().verifyToLedgerTransaction(MockIdentityService, attachments)
|
||||
}
|
||||
|
||||
val tooEarlyClose = makeFundedTX(TEST_TX_TIME + 6.days)
|
||||
val validClose = makeFundedTX(TEST_TX_TIME + 8.days)
|
||||
|
||||
val e = assertFailsWith(TransactionVerificationException::class) {
|
||||
TransactionGroup(setOf(registerTX, pledgeTX, tooEarlyClose), setOf(miniCorpWalletTx, aliceWalletTX)).verify()
|
||||
}
|
||||
assertTrue(e.cause!!.message!!.contains("the closing date has past"))
|
||||
|
||||
// This verification passes
|
||||
TransactionGroup(setOf(registerTX, pledgeTX, validClose), setOf(aliceWalletTX)).verify()
|
||||
}
|
||||
}
|
@ -1,733 +0,0 @@
|
||||
package contracts
|
||||
|
||||
import core.*
|
||||
import core.contracts.*
|
||||
import core.testutils.*
|
||||
import org.junit.Test
|
||||
import java.math.BigDecimal
|
||||
import java.time.LocalDate
|
||||
import java.util.*
|
||||
|
||||
fun createDummyIRS(irsSelect: Int): InterestRateSwap.State {
|
||||
return when (irsSelect) {
|
||||
1 -> {
|
||||
|
||||
val fixedLeg = InterestRateSwap.FixedLeg(
|
||||
fixedRatePayer = MEGA_CORP,
|
||||
notional = 15900000.DOLLARS,
|
||||
paymentFrequency = Frequency.SemiAnnual,
|
||||
effectiveDate = LocalDate.of(2016, 3, 10),
|
||||
effectiveDateAdjustment = null,
|
||||
terminationDate = LocalDate.of(2026, 3, 10),
|
||||
terminationDateAdjustment = null,
|
||||
fixedRate = FixedRate(PercentageRatioUnit("1.677")),
|
||||
dayCountBasisDay = DayCountBasisDay.D30,
|
||||
dayCountBasisYear = DayCountBasisYear.Y360,
|
||||
rollConvention = DateRollConvention.ModifiedFollowing,
|
||||
dayInMonth = 10,
|
||||
paymentRule = PaymentRule.InArrears,
|
||||
paymentDelay = 0,
|
||||
paymentCalendar = BusinessCalendar.getInstance("London", "NewYork"),
|
||||
interestPeriodAdjustment = AccrualAdjustment.Adjusted
|
||||
)
|
||||
|
||||
val floatingLeg = InterestRateSwap.FloatingLeg(
|
||||
floatingRatePayer = MINI_CORP,
|
||||
notional = 15900000.DOLLARS,
|
||||
paymentFrequency = Frequency.Quarterly,
|
||||
effectiveDate = LocalDate.of(2016, 3, 10),
|
||||
effectiveDateAdjustment = null,
|
||||
terminationDate = LocalDate.of(2026, 3, 10),
|
||||
terminationDateAdjustment = null,
|
||||
dayCountBasisDay = DayCountBasisDay.D30,
|
||||
dayCountBasisYear = DayCountBasisYear.Y360,
|
||||
rollConvention = DateRollConvention.ModifiedFollowing,
|
||||
fixingRollConvention = DateRollConvention.ModifiedFollowing,
|
||||
dayInMonth = 10,
|
||||
resetDayInMonth = 10,
|
||||
paymentRule = PaymentRule.InArrears,
|
||||
paymentDelay = 0,
|
||||
paymentCalendar = BusinessCalendar.getInstance("London", "NewYork"),
|
||||
interestPeriodAdjustment = AccrualAdjustment.Adjusted,
|
||||
fixingPeriod = DateOffset.TWODAYS,
|
||||
resetRule = PaymentRule.InAdvance,
|
||||
fixingsPerPayment = Frequency.Quarterly,
|
||||
fixingCalendar = BusinessCalendar.getInstance("London"),
|
||||
index = "LIBOR",
|
||||
indexSource = "TEL3750",
|
||||
indexTenor = Tenor("3M")
|
||||
)
|
||||
|
||||
val calculation = InterestRateSwap.Calculation (
|
||||
|
||||
// TODO: this seems to fail quite dramatically
|
||||
//expression = "fixedLeg.notional * fixedLeg.fixedRate",
|
||||
|
||||
// TODO: How I want it to look
|
||||
//expression = "( fixedLeg.notional * (fixedLeg.fixedRate)) - (floatingLeg.notional * (rateSchedule.get(context.getDate('currentDate'))))",
|
||||
|
||||
// How it's ended up looking, which I think is now broken but it's a WIP.
|
||||
expression = Expression("( fixedLeg.notional.pennies * (fixedLeg.fixedRate.ratioUnit.value)) -" +
|
||||
"(floatingLeg.notional.pennies * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))"),
|
||||
|
||||
floatingLegPaymentSchedule = HashMap(),
|
||||
fixedLegPaymentSchedule = HashMap()
|
||||
)
|
||||
|
||||
val EUR = currency("EUR")
|
||||
|
||||
val common = InterestRateSwap.Common(
|
||||
baseCurrency = EUR,
|
||||
eligibleCurrency = EUR,
|
||||
eligibleCreditSupport = "Cash in an Eligible Currency",
|
||||
independentAmounts = Amount(0, EUR),
|
||||
threshold = Amount(0, EUR),
|
||||
minimumTransferAmount = Amount(250000 * 100, EUR),
|
||||
rounding = Amount(10000 * 100, EUR),
|
||||
valuationDate = "Every Local Business Day",
|
||||
notificationTime = "2:00pm London",
|
||||
resolutionTime = "2:00pm London time on the first LocalBusiness Day following the date on which the notice is given ",
|
||||
interestRate = ReferenceRate("T3270", Tenor("6M"), "EONIA"),
|
||||
addressForTransfers = "",
|
||||
exposure = UnknownType(),
|
||||
localBusinessDay = BusinessCalendar.getInstance("London"),
|
||||
tradeID = "trade1",
|
||||
hashLegalDocs = "put hash here",
|
||||
dailyInterestAmount = Expression("(CashAmount * InterestRate ) / (fixedLeg.notional.currency.currencyCode.equals('GBP')) ? 365 : 360")
|
||||
)
|
||||
|
||||
InterestRateSwap.State(fixedLeg = fixedLeg, floatingLeg = floatingLeg, calculation = calculation, common = common, notary = DUMMY_NOTARY)
|
||||
}
|
||||
2 -> {
|
||||
// 10y swap, we pay 1.3% fixed 30/360 semi, rec 3m usd libor act/360 Q on 25m notional (mod foll/adj on both sides)
|
||||
// I did a mock up start date 10/03/2015 – 10/03/2025 so you have 5 cashflows on float side that have been preset the rest are unknown
|
||||
|
||||
val fixedLeg = InterestRateSwap.FixedLeg(
|
||||
fixedRatePayer = MEGA_CORP,
|
||||
notional = 25000000.DOLLARS,
|
||||
paymentFrequency = Frequency.SemiAnnual,
|
||||
effectiveDate = LocalDate.of(2015, 3, 10),
|
||||
effectiveDateAdjustment = null,
|
||||
terminationDate = LocalDate.of(2025, 3, 10),
|
||||
terminationDateAdjustment = null,
|
||||
fixedRate = FixedRate(PercentageRatioUnit("1.3")),
|
||||
dayCountBasisDay = DayCountBasisDay.D30,
|
||||
dayCountBasisYear = DayCountBasisYear.Y360,
|
||||
rollConvention = DateRollConvention.ModifiedFollowing,
|
||||
dayInMonth = 10,
|
||||
paymentRule = PaymentRule.InArrears,
|
||||
paymentDelay = 0,
|
||||
paymentCalendar = BusinessCalendar.getInstance(),
|
||||
interestPeriodAdjustment = AccrualAdjustment.Adjusted
|
||||
)
|
||||
|
||||
val floatingLeg = InterestRateSwap.FloatingLeg(
|
||||
floatingRatePayer = MINI_CORP,
|
||||
notional = 25000000.DOLLARS,
|
||||
paymentFrequency = Frequency.Quarterly,
|
||||
effectiveDate = LocalDate.of(2015, 3, 10),
|
||||
effectiveDateAdjustment = null,
|
||||
terminationDate = LocalDate.of(2025, 3, 10),
|
||||
terminationDateAdjustment = null,
|
||||
dayCountBasisDay = DayCountBasisDay.DActual,
|
||||
dayCountBasisYear = DayCountBasisYear.Y360,
|
||||
rollConvention = DateRollConvention.ModifiedFollowing,
|
||||
fixingRollConvention = DateRollConvention.ModifiedFollowing,
|
||||
dayInMonth = 10,
|
||||
resetDayInMonth = 10,
|
||||
paymentRule = PaymentRule.InArrears,
|
||||
paymentDelay = 0,
|
||||
paymentCalendar = BusinessCalendar.getInstance(),
|
||||
interestPeriodAdjustment = AccrualAdjustment.Adjusted,
|
||||
fixingPeriod = DateOffset.TWODAYS,
|
||||
resetRule = PaymentRule.InAdvance,
|
||||
fixingsPerPayment = Frequency.Quarterly,
|
||||
fixingCalendar = BusinessCalendar.getInstance(),
|
||||
index = "USD LIBOR",
|
||||
indexSource = "TEL3750",
|
||||
indexTenor = Tenor("3M")
|
||||
)
|
||||
|
||||
val calculation = InterestRateSwap.Calculation (
|
||||
|
||||
// TODO: this seems to fail quite dramatically
|
||||
//expression = "fixedLeg.notional * fixedLeg.fixedRate",
|
||||
|
||||
// TODO: How I want it to look
|
||||
//expression = "( fixedLeg.notional * (fixedLeg.fixedRate)) - (floatingLeg.notional * (rateSchedule.get(context.getDate('currentDate'))))",
|
||||
|
||||
// How it's ended up looking, which I think is now broken but it's a WIP.
|
||||
expression = Expression("( fixedLeg.notional.pennies * (fixedLeg.fixedRate.ratioUnit.value)) -" +
|
||||
"(floatingLeg.notional.pennies * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))"),
|
||||
|
||||
floatingLegPaymentSchedule = HashMap(),
|
||||
fixedLegPaymentSchedule = HashMap()
|
||||
)
|
||||
|
||||
val EUR = currency("EUR")
|
||||
|
||||
val common = InterestRateSwap.Common(
|
||||
baseCurrency = EUR,
|
||||
eligibleCurrency = EUR,
|
||||
eligibleCreditSupport = "Cash in an Eligible Currency",
|
||||
independentAmounts = Amount(0, EUR),
|
||||
threshold = Amount(0, EUR),
|
||||
minimumTransferAmount = Amount(250000 * 100, EUR),
|
||||
rounding = Amount(10000 * 100, EUR),
|
||||
valuationDate = "Every Local Business Day",
|
||||
notificationTime = "2:00pm London",
|
||||
resolutionTime = "2:00pm London time on the first LocalBusiness Day following the date on which the notice is given ",
|
||||
interestRate = ReferenceRate("T3270", Tenor("6M"), "EONIA"),
|
||||
addressForTransfers = "",
|
||||
exposure = UnknownType(),
|
||||
localBusinessDay = BusinessCalendar.getInstance("London"),
|
||||
tradeID = "trade2",
|
||||
hashLegalDocs = "put hash here",
|
||||
dailyInterestAmount = Expression("(CashAmount * InterestRate ) / (fixedLeg.notional.currency.currencyCode.equals('GBP')) ? 365 : 360")
|
||||
)
|
||||
|
||||
return InterestRateSwap.State(fixedLeg = fixedLeg, floatingLeg = floatingLeg, calculation = calculation, common = common, notary = DUMMY_NOTARY)
|
||||
|
||||
}
|
||||
else -> TODO("IRS number $irsSelect not defined")
|
||||
}
|
||||
}
|
||||
|
||||
class IRSTests {
|
||||
|
||||
val attachments = MockStorageService().attachments
|
||||
|
||||
val exampleIRS = createDummyIRS(1)
|
||||
|
||||
val inState = InterestRateSwap.State(
|
||||
exampleIRS.fixedLeg,
|
||||
exampleIRS.floatingLeg,
|
||||
exampleIRS.calculation,
|
||||
exampleIRS.common,
|
||||
DUMMY_NOTARY
|
||||
)
|
||||
|
||||
val outState = inState.copy()
|
||||
|
||||
@Test
|
||||
fun ok() {
|
||||
trade().verify()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ok with groups`() {
|
||||
tradegroups().verify()
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an IRS txn - we'll need it for a few things.
|
||||
*/
|
||||
fun generateIRSTxn(irsSelect: Int): LedgerTransaction {
|
||||
val dummyIRS = createDummyIRS(irsSelect)
|
||||
val genTX: LedgerTransaction = run {
|
||||
val gtx = InterestRateSwap().generateAgreement(
|
||||
fixedLeg = dummyIRS.fixedLeg,
|
||||
floatingLeg = dummyIRS.floatingLeg,
|
||||
calculation = dummyIRS.calculation,
|
||||
common = dummyIRS.common,
|
||||
notary = DUMMY_NOTARY).apply {
|
||||
setTime(TEST_TX_TIME, DUMMY_NOTARY, 30.seconds)
|
||||
signWith(MEGA_CORP_KEY)
|
||||
signWith(MINI_CORP_KEY)
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
}
|
||||
gtx.toSignedTransaction().verifyToLedgerTransaction(MockIdentityService, attachments)
|
||||
}
|
||||
return genTX
|
||||
}
|
||||
|
||||
/**
|
||||
* Just make sure it's sane.
|
||||
*/
|
||||
@Test
|
||||
fun pprintIRS() {
|
||||
val irs = singleIRS()
|
||||
println(irs.prettyPrint())
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility so I don't have to keep typing this
|
||||
*/
|
||||
fun singleIRS(irsSelector: Int = 1): InterestRateSwap.State {
|
||||
return generateIRSTxn(irsSelector).outputs.filterIsInstance<InterestRateSwap.State>().single()
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the generate. No explicit exception as if something goes wrong, we'll find out anyway.
|
||||
*/
|
||||
@Test
|
||||
fun generateIRS() {
|
||||
// Tests aren't allowed to return things
|
||||
generateIRSTxn(1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Testing a simple IRS, add a few fixings and then display as CSV
|
||||
*/
|
||||
@Test
|
||||
fun `IRS Export test`() {
|
||||
// No transactions etc required - we're just checking simple maths and export functionallity
|
||||
val irs = singleIRS(2)
|
||||
|
||||
var newCalculation = irs.calculation
|
||||
|
||||
val fixings = mapOf(LocalDate.of(2015, 3, 6) to "0.6",
|
||||
LocalDate.of(2015, 6, 8) to "0.75",
|
||||
LocalDate.of(2015, 9, 8) to "0.8",
|
||||
LocalDate.of(2015, 12, 8) to "0.55",
|
||||
LocalDate.of(2016, 3, 8) to "0.644")
|
||||
|
||||
for (it in fixings) {
|
||||
newCalculation = newCalculation.applyFixing(it.key, FixedRate(PercentageRatioUnit(it.value)))
|
||||
}
|
||||
|
||||
val newIRS = InterestRateSwap.State(irs.fixedLeg, irs.floatingLeg, newCalculation, irs.common, DUMMY_NOTARY)
|
||||
println(newIRS.exportIRSToCSV())
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure it has a schedule and the schedule has some unfixed rates
|
||||
*/
|
||||
@Test
|
||||
fun `next fixing date`() {
|
||||
val irs = singleIRS(1)
|
||||
println(irs.calculation.nextFixingDate())
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate through all the fix dates and add something
|
||||
*/
|
||||
@Test
|
||||
fun generateIRSandFixSome() {
|
||||
var previousTXN = generateIRSTxn(1)
|
||||
var currentIRS = previousTXN.outputs.filterIsInstance<InterestRateSwap.State>().single()
|
||||
println(currentIRS.prettyPrint())
|
||||
while (true) {
|
||||
val nextFixingDate = currentIRS.calculation.nextFixingDate() ?: break
|
||||
println("\n\n\n ***** Applying a fixing to $nextFixingDate \n\n\n")
|
||||
var fixTX: LedgerTransaction = run {
|
||||
val tx = TransactionBuilder()
|
||||
val fixing = Pair(nextFixingDate, FixedRate("0.052".percent))
|
||||
InterestRateSwap().generateFix(tx, previousTXN.outRef(0), fixing)
|
||||
with(tx) {
|
||||
setTime(TEST_TX_TIME, DUMMY_NOTARY, 30.seconds)
|
||||
signWith(MEGA_CORP_KEY)
|
||||
signWith(MINI_CORP_KEY)
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
}
|
||||
tx.toSignedTransaction().verifyToLedgerTransaction(MockIdentityService, attachments)
|
||||
}
|
||||
currentIRS = previousTXN.outputs.filterIsInstance<InterestRateSwap.State>().single()
|
||||
println(currentIRS.prettyPrint())
|
||||
previousTXN = fixTX
|
||||
}
|
||||
}
|
||||
|
||||
// Move these later as they aren't IRS specific.
|
||||
@Test
|
||||
fun `test some rate objects 100 * FixedRate(5%)`() {
|
||||
val r1 = FixedRate(PercentageRatioUnit("5"))
|
||||
assert(100 * r1 == 5)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `expression calculation testing`() {
|
||||
val dummyIRS = singleIRS()
|
||||
val stuffToPrint: ArrayList<String> = arrayListOf(
|
||||
"fixedLeg.notional.pennies",
|
||||
"fixedLeg.fixedRate.ratioUnit",
|
||||
"fixedLeg.fixedRate.ratioUnit.value",
|
||||
"floatingLeg.notional.pennies",
|
||||
"fixedLeg.fixedRate",
|
||||
"currentBusinessDate",
|
||||
"calculation.floatingLegPaymentSchedule.get(currentBusinessDate)",
|
||||
"fixedLeg.notional.currency.currencyCode",
|
||||
"fixedLeg.notional.pennies * 10",
|
||||
"fixedLeg.notional.pennies * fixedLeg.fixedRate.ratioUnit.value",
|
||||
"(fixedLeg.notional.currency.currencyCode.equals('GBP')) ? 365 : 360 ",
|
||||
"(fixedLeg.notional.pennies * (fixedLeg.fixedRate.ratioUnit.value))"
|
||||
// "calculation.floatingLegPaymentSchedule.get(context.getDate('currentDate')).rate"
|
||||
// "calculation.floatingLegPaymentSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value",
|
||||
//"( fixedLeg.notional.pennies * (fixedLeg.fixedRate.ratioUnit.value)) - (floatingLeg.notional.pennies * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))",
|
||||
// "( fixedLeg.notional * fixedLeg.fixedRate )"
|
||||
)
|
||||
|
||||
for (i in stuffToPrint) {
|
||||
println(i)
|
||||
var z = dummyIRS.evaluateCalculation(LocalDate.of(2016, 9, 12), Expression(i))
|
||||
println(z.javaClass)
|
||||
println(z)
|
||||
println("-----------")
|
||||
}
|
||||
// This does not throw an exception in the test itself; it evaluates the above and they will throw if they do not pass.
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generates a typical transactional history for an IRS.
|
||||
*/
|
||||
fun trade(): TransactionGroupDSL<InterestRateSwap.State> {
|
||||
|
||||
val ld = LocalDate.of(2016, 3, 8)
|
||||
val bd = BigDecimal("0.0063518")
|
||||
|
||||
val txgroup: TransactionGroupDSL<InterestRateSwap.State> = transactionGroupFor() {
|
||||
transaction("Agreement") {
|
||||
output("irs post agreement") { singleIRS() }
|
||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
transaction("Fix") {
|
||||
input("irs post agreement")
|
||||
output("irs post first fixing") {
|
||||
"irs post agreement".output.copy(
|
||||
"irs post agreement".output.fixedLeg,
|
||||
"irs post agreement".output.floatingLeg,
|
||||
"irs post agreement".output.calculation.applyFixing(ld, FixedRate(RatioUnit(bd))),
|
||||
"irs post agreement".output.common
|
||||
)
|
||||
}
|
||||
arg(ORACLE_PUBKEY) {
|
||||
InterestRateSwap.Commands.Fix()
|
||||
}
|
||||
arg(ORACLE_PUBKEY) {
|
||||
Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)
|
||||
}
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
}
|
||||
return txgroup
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ensure failure occurs when there are inbound states for an agreement command`() {
|
||||
transaction {
|
||||
input() { singleIRS() }
|
||||
output("irs post agreement") { singleIRS() }
|
||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails requirement` "There are no in states for an agreement"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ensure failure occurs when no events in fix schedule`() {
|
||||
val irs = singleIRS()
|
||||
val emptySchedule = HashMap<LocalDate, FixedRatePaymentEvent>()
|
||||
transaction {
|
||||
output() {
|
||||
irs.copy(calculation = irs.calculation.copy(fixedLegPaymentSchedule = emptySchedule))
|
||||
}
|
||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails requirement` "There are events in the fix schedule"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ensure failure occurs when no events in floating schedule`() {
|
||||
val irs = singleIRS()
|
||||
val emptySchedule = HashMap<LocalDate, FloatingRatePaymentEvent>()
|
||||
transaction {
|
||||
output() {
|
||||
irs.copy(calculation = irs.calculation.copy(floatingLegPaymentSchedule = emptySchedule))
|
||||
}
|
||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails requirement` "There are events in the float schedule"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ensure notionals are non zero`() {
|
||||
val irs = singleIRS()
|
||||
transaction {
|
||||
output() {
|
||||
irs.copy(irs.fixedLeg.copy(notional = irs.fixedLeg.notional.copy(pennies = 0)))
|
||||
}
|
||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails requirement` "All notionals must be non zero"
|
||||
}
|
||||
|
||||
transaction {
|
||||
output() {
|
||||
irs.copy(irs.fixedLeg.copy(notional = irs.floatingLeg.notional.copy(pennies = 0)))
|
||||
}
|
||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails requirement` "All notionals must be non zero"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ensure positive rate on fixed leg`() {
|
||||
val irs = singleIRS()
|
||||
val modifiedIRS = irs.copy(fixedLeg = irs.fixedLeg.copy(fixedRate = FixedRate(PercentageRatioUnit("-0.1"))))
|
||||
transaction {
|
||||
output() {
|
||||
modifiedIRS
|
||||
}
|
||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails requirement` "The fixed leg rate must be positive"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This will be modified once we adapt the IRS to be cross currency
|
||||
*/
|
||||
@Test
|
||||
fun `ensure same currency notionals`() {
|
||||
val irs = singleIRS()
|
||||
val modifiedIRS = irs.copy(fixedLeg = irs.fixedLeg.copy(notional = Amount(irs.fixedLeg.notional.pennies, Currency.getInstance("JPY"))))
|
||||
transaction {
|
||||
output() {
|
||||
modifiedIRS
|
||||
}
|
||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails requirement` "The currency of the notionals must be the same"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ensure notional amounts are equal`() {
|
||||
val irs = singleIRS()
|
||||
val modifiedIRS = irs.copy(fixedLeg = irs.fixedLeg.copy(notional = Amount(irs.floatingLeg.notional.pennies + 1, irs.floatingLeg.notional.currency)))
|
||||
transaction {
|
||||
output() {
|
||||
modifiedIRS
|
||||
}
|
||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails requirement` "All leg notionals must be the same"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ensure trade date and termination date checks are done pt1`() {
|
||||
val irs = singleIRS()
|
||||
val modifiedIRS1 = irs.copy(fixedLeg = irs.fixedLeg.copy(terminationDate = irs.fixedLeg.effectiveDate.minusDays(1)))
|
||||
transaction {
|
||||
output() {
|
||||
modifiedIRS1
|
||||
}
|
||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails requirement` "The effective date is before the termination date for the fixed leg"
|
||||
}
|
||||
|
||||
val modifiedIRS2 = irs.copy(floatingLeg = irs.floatingLeg.copy(terminationDate = irs.floatingLeg.effectiveDate.minusDays(1)))
|
||||
transaction {
|
||||
output() {
|
||||
modifiedIRS2
|
||||
}
|
||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails requirement` "The effective date is before the termination date for the floating leg"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ensure trade date and termination date checks are done pt2`() {
|
||||
val irs = singleIRS()
|
||||
|
||||
val modifiedIRS3 = irs.copy(floatingLeg = irs.floatingLeg.copy(terminationDate = irs.fixedLeg.terminationDate.minusDays(1)))
|
||||
transaction {
|
||||
output() {
|
||||
modifiedIRS3
|
||||
}
|
||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails requirement` "The termination dates are aligned"
|
||||
}
|
||||
|
||||
|
||||
val modifiedIRS4 = irs.copy(floatingLeg = irs.floatingLeg.copy(effectiveDate = irs.fixedLeg.effectiveDate.minusDays(1)))
|
||||
transaction {
|
||||
output() {
|
||||
modifiedIRS4
|
||||
}
|
||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails requirement` "The effective dates are aligned"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `various fixing tests`() {
|
||||
|
||||
val ld = LocalDate.of(2016, 3, 8)
|
||||
val bd = BigDecimal("0.0063518")
|
||||
|
||||
transaction {
|
||||
output("irs post agreement") { singleIRS() }
|
||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
val oldIRS = singleIRS(1)
|
||||
val newIRS = oldIRS.copy(oldIRS.fixedLeg,
|
||||
oldIRS.floatingLeg,
|
||||
oldIRS.calculation.applyFixing(ld, FixedRate(RatioUnit(bd))),
|
||||
oldIRS.common)
|
||||
|
||||
transaction {
|
||||
input() {
|
||||
oldIRS
|
||||
|
||||
}
|
||||
|
||||
// Templated tweak for reference. A corrent fixing applied should be ok
|
||||
tweak {
|
||||
arg(ORACLE_PUBKEY) {
|
||||
InterestRateSwap.Commands.Fix()
|
||||
}
|
||||
timestamp(TEST_TX_TIME)
|
||||
arg(ORACLE_PUBKEY) {
|
||||
Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)
|
||||
}
|
||||
output() { newIRS }
|
||||
this.accepts()
|
||||
}
|
||||
|
||||
// This test makes sure that verify confirms the fixing was applied and there is a difference in the old and new
|
||||
tweak {
|
||||
arg(ORACLE_PUBKEY) { InterestRateSwap.Commands.Fix() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
arg(ORACLE_PUBKEY) { Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd) }
|
||||
output() { oldIRS }
|
||||
this`fails requirement` "There is at least one difference in the IRS floating leg payment schedules"
|
||||
}
|
||||
|
||||
// This tests tries to sneak in a change to another fixing (which may or may not be the latest one)
|
||||
tweak {
|
||||
arg(ORACLE_PUBKEY) { InterestRateSwap.Commands.Fix() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
arg(ORACLE_PUBKEY) {
|
||||
Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)
|
||||
}
|
||||
|
||||
val firstResetKey = newIRS.calculation.floatingLegPaymentSchedule.keys.first()
|
||||
val firstResetValue = newIRS.calculation.floatingLegPaymentSchedule[firstResetKey]
|
||||
var modifiedFirstResetValue = firstResetValue!!.copy(notional = Amount(firstResetValue.notional.pennies, Currency.getInstance("JPY")))
|
||||
|
||||
output() {
|
||||
newIRS.copy(
|
||||
newIRS.fixedLeg,
|
||||
newIRS.floatingLeg,
|
||||
newIRS.calculation.copy(floatingLegPaymentSchedule = newIRS.calculation.floatingLegPaymentSchedule.plus(
|
||||
Pair(firstResetKey, modifiedFirstResetValue))),
|
||||
newIRS.common
|
||||
)
|
||||
}
|
||||
this`fails requirement` "There is only one change in the IRS floating leg payment schedule"
|
||||
}
|
||||
|
||||
// This tests modifies the payment currency for the fixing
|
||||
tweak {
|
||||
arg(ORACLE_PUBKEY) { InterestRateSwap.Commands.Fix() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
arg(ORACLE_PUBKEY) { Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd) }
|
||||
|
||||
val latestReset = newIRS.calculation.floatingLegPaymentSchedule.filter { it.value.rate is FixedRate }.maxBy { it.key }
|
||||
var modifiedLatestResetValue = latestReset!!.value.copy(notional = Amount(latestReset.value.notional.pennies, Currency.getInstance("JPY")))
|
||||
|
||||
output() {
|
||||
newIRS.copy(
|
||||
newIRS.fixedLeg,
|
||||
newIRS.floatingLeg,
|
||||
newIRS.calculation.copy(floatingLegPaymentSchedule = newIRS.calculation.floatingLegPaymentSchedule.plus(
|
||||
Pair(latestReset.key, modifiedLatestResetValue))),
|
||||
newIRS.common
|
||||
)
|
||||
}
|
||||
this`fails requirement` "The fix payment has the same currency as the notional"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This returns an example of transactions that are grouped by TradeId and then a fixing applied.
|
||||
* It's important to make the tradeID different for two reasons, the hashes will be the same and all sorts of confusion will
|
||||
* result and the grouping won't work either.
|
||||
* In reality, the only fields that should be in common will be the next fixing date and the reference rate.
|
||||
*/
|
||||
fun tradegroups(): TransactionGroupDSL<InterestRateSwap.State> {
|
||||
val ld1 = LocalDate.of(2016, 3, 8)
|
||||
val bd1 = BigDecimal("0.0063518")
|
||||
|
||||
val irs = singleIRS()
|
||||
|
||||
val txgroup: TransactionGroupDSL<InterestRateSwap.State> = transactionGroupFor() {
|
||||
transaction("Agreement") {
|
||||
output("irs post agreement1") {
|
||||
irs.copy(
|
||||
irs.fixedLeg,
|
||||
irs.floatingLeg,
|
||||
irs.calculation,
|
||||
irs.common.copy(tradeID = "t1")
|
||||
|
||||
)
|
||||
}
|
||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
transaction("Agreement") {
|
||||
output("irs post agreement2") {
|
||||
irs.copy(
|
||||
irs.fixedLeg,
|
||||
irs.floatingLeg,
|
||||
irs.calculation,
|
||||
irs.common.copy(tradeID = "t2")
|
||||
|
||||
)
|
||||
}
|
||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
transaction("Fix") {
|
||||
input("irs post agreement1")
|
||||
input("irs post agreement2")
|
||||
output("irs post first fixing1") {
|
||||
"irs post agreement1".output.copy(
|
||||
"irs post agreement1".output.fixedLeg,
|
||||
"irs post agreement1".output.floatingLeg,
|
||||
"irs post agreement1".output.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))),
|
||||
"irs post agreement1".output.common.copy(tradeID = "t1")
|
||||
)
|
||||
}
|
||||
output("irs post first fixing2") {
|
||||
"irs post agreement2".output.copy(
|
||||
"irs post agreement2".output.fixedLeg,
|
||||
"irs post agreement2".output.floatingLeg,
|
||||
"irs post agreement2".output.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))),
|
||||
"irs post agreement2".output.common.copy(tradeID = "t2")
|
||||
)
|
||||
}
|
||||
|
||||
arg(ORACLE_PUBKEY) {
|
||||
InterestRateSwap.Commands.Fix()
|
||||
}
|
||||
arg(ORACLE_PUBKEY) {
|
||||
Fix(FixOf("ICE LIBOR", ld1, Tenor("3M")), bd1)
|
||||
}
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
}
|
||||
return txgroup
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,130 +0,0 @@
|
||||
package core
|
||||
|
||||
import com.codahale.metrics.MetricRegistry
|
||||
import core.contracts.Attachment
|
||||
import core.crypto.SecureHash
|
||||
import core.crypto.generateKeyPair
|
||||
import core.crypto.sha256
|
||||
import core.messaging.MessagingService
|
||||
import core.node.ServiceHub
|
||||
import core.node.storage.Checkpoint
|
||||
import core.node.storage.CheckpointStorage
|
||||
import core.node.subsystems.*
|
||||
import core.node.services.AttachmentStorage
|
||||
import core.node.services.IdentityService
|
||||
import core.node.services.NetworkMapService
|
||||
import core.testing.MockNetworkMapCache
|
||||
import core.testutils.MockIdentityService
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.security.KeyPair
|
||||
import java.security.PrivateKey
|
||||
import java.security.PublicKey
|
||||
import java.time.Clock
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
import java.util.jar.JarInputStream
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
|
||||
class MockKeyManagementService(vararg initialKeys: KeyPair) : KeyManagementService {
|
||||
override val keys: MutableMap<PublicKey, PrivateKey>
|
||||
|
||||
init {
|
||||
keys = initialKeys.map { it.public to it.private }.toMap(HashMap())
|
||||
}
|
||||
|
||||
val nextKeys = LinkedList<KeyPair>()
|
||||
|
||||
override fun freshKey(): KeyPair {
|
||||
val k = nextKeys.poll() ?: generateKeyPair()
|
||||
keys[k.public] = k.private
|
||||
return k
|
||||
}
|
||||
}
|
||||
|
||||
class MockAttachmentStorage : AttachmentStorage {
|
||||
val files = HashMap<SecureHash, ByteArray>()
|
||||
|
||||
override fun openAttachment(id: SecureHash): Attachment? {
|
||||
val f = files[id] ?: return null
|
||||
return object : Attachment {
|
||||
override fun open(): InputStream = ByteArrayInputStream(f)
|
||||
override val id: SecureHash = id
|
||||
}
|
||||
}
|
||||
|
||||
override fun importAttachment(jar: InputStream): SecureHash {
|
||||
// JIS makes read()/readBytes() return bytes of the current file, but we want to hash the entire container here.
|
||||
require(jar !is JarInputStream)
|
||||
|
||||
val bytes = run {
|
||||
val s = ByteArrayOutputStream()
|
||||
jar.copyTo(s)
|
||||
s.close()
|
||||
s.toByteArray()
|
||||
}
|
||||
val sha256 = bytes.sha256()
|
||||
if (files.containsKey(sha256))
|
||||
throw FileAlreadyExistsException(File("!! MOCK FILE NAME"))
|
||||
files[sha256] = bytes
|
||||
return sha256
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class MockCheckpointStorage : CheckpointStorage {
|
||||
|
||||
private val _checkpoints = ConcurrentLinkedQueue<Checkpoint>()
|
||||
override val checkpoints: Iterable<Checkpoint>
|
||||
get() = _checkpoints.toList()
|
||||
|
||||
override fun addCheckpoint(checkpoint: Checkpoint) {
|
||||
_checkpoints.add(checkpoint)
|
||||
}
|
||||
|
||||
override fun removeCheckpoint(checkpoint: Checkpoint) {
|
||||
require(_checkpoints.remove(checkpoint))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ThreadSafe
|
||||
class MockStorageService : StorageServiceImpl(MockAttachmentStorage(), generateKeyPair())
|
||||
|
||||
class MockServices(
|
||||
customWallet: WalletService? = null,
|
||||
val keyManagement: KeyManagementService? = null,
|
||||
val net: MessagingService? = null,
|
||||
val identity: IdentityService? = MockIdentityService,
|
||||
val storage: StorageService? = MockStorageService(),
|
||||
val mapCache: NetworkMapCache? = MockNetworkMapCache(),
|
||||
val mapService: NetworkMapService? = null,
|
||||
val overrideClock: Clock? = Clock.systemUTC()
|
||||
) : ServiceHub {
|
||||
override val walletService: WalletService = customWallet ?: NodeWalletService(this)
|
||||
|
||||
override val keyManagementService: KeyManagementService
|
||||
get() = keyManagement ?: throw UnsupportedOperationException()
|
||||
override val identityService: IdentityService
|
||||
get() = identity ?: throw UnsupportedOperationException()
|
||||
override val networkService: MessagingService
|
||||
get() = net ?: throw UnsupportedOperationException()
|
||||
override val networkMapCache: NetworkMapCache
|
||||
get() = mapCache ?: throw UnsupportedOperationException()
|
||||
override val storageService: StorageService
|
||||
get() = storage ?: throw UnsupportedOperationException()
|
||||
override val clock: Clock
|
||||
get() = overrideClock ?: throw UnsupportedOperationException()
|
||||
|
||||
override val monitoringService: MonitoringService = MonitoringService(MetricRegistry())
|
||||
|
||||
init {
|
||||
if (net != null && storage != null) {
|
||||
// Creating this class is sufficient, we don't have to store it anywhere, because it registers a listener
|
||||
// on the networking service, so that will keep it from being collected.
|
||||
DataVendingService(net, storage)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,152 +0,0 @@
|
||||
package core
|
||||
|
||||
import contracts.Cash
|
||||
import core.contracts.*
|
||||
import core.testutils.*
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertNotEquals
|
||||
|
||||
class TransactionGroupTests {
|
||||
val A_THOUSAND_POUNDS = Cash.State(MINI_CORP.ref(1, 2, 3), 1000.POUNDS, MINI_CORP_PUBKEY, DUMMY_NOTARY)
|
||||
|
||||
@Test
|
||||
fun success() {
|
||||
transactionGroup {
|
||||
roots {
|
||||
transaction(A_THOUSAND_POUNDS label "£1000")
|
||||
}
|
||||
|
||||
transaction {
|
||||
input("£1000")
|
||||
output("alice's £1000") { A_THOUSAND_POUNDS `owned by` ALICE_PUBKEY }
|
||||
arg(MINI_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
}
|
||||
|
||||
transaction {
|
||||
input("alice's £1000")
|
||||
arg(ALICE_PUBKEY) { Cash.Commands.Move() }
|
||||
arg(MINI_CORP_PUBKEY) { Cash.Commands.Exit(1000.POUNDS) }
|
||||
}
|
||||
|
||||
verify()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun conflict() {
|
||||
transactionGroup {
|
||||
val t = transaction {
|
||||
output("cash") { A_THOUSAND_POUNDS }
|
||||
arg(MINI_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||
}
|
||||
|
||||
val conflict1 = transaction {
|
||||
input("cash")
|
||||
val HALF = A_THOUSAND_POUNDS.copy(amount = 500.POUNDS) `owned by` BOB_PUBKEY
|
||||
output { HALF }
|
||||
output { HALF }
|
||||
arg(MINI_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
}
|
||||
|
||||
verify()
|
||||
|
||||
// Alice tries to double spend back to herself.
|
||||
val conflict2 = transaction {
|
||||
input("cash")
|
||||
val HALF = A_THOUSAND_POUNDS.copy(amount = 500.POUNDS) `owned by` ALICE_PUBKEY
|
||||
output { HALF }
|
||||
output { HALF }
|
||||
arg(MINI_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
}
|
||||
|
||||
assertNotEquals(conflict1, conflict2)
|
||||
|
||||
val e = assertFailsWith(TransactionConflictException::class) {
|
||||
verify()
|
||||
}
|
||||
assertEquals(StateRef(t.id, 0), e.conflictRef)
|
||||
assertEquals(setOf(conflict1.id, conflict2.id), setOf(e.tx1.id, e.tx2.id))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun disconnected() {
|
||||
// Check that if we have a transaction in the group that doesn't connect to anything else, it's rejected.
|
||||
val tg = transactionGroup {
|
||||
transaction {
|
||||
output("cash") { A_THOUSAND_POUNDS }
|
||||
arg(MINI_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||
}
|
||||
|
||||
transaction {
|
||||
input("cash")
|
||||
output { A_THOUSAND_POUNDS `owned by` BOB_PUBKEY }
|
||||
}
|
||||
}
|
||||
|
||||
// We have to do this manually without the DSL because transactionGroup { } won't let us create a tx that
|
||||
// points nowhere.
|
||||
val input = generateStateRef()
|
||||
tg.txns += TransactionBuilder().apply {
|
||||
addInputState(input)
|
||||
addOutputState(A_THOUSAND_POUNDS)
|
||||
addCommand(Cash.Commands.Move(), BOB_PUBKEY)
|
||||
}.toWireTransaction()
|
||||
|
||||
val e = assertFailsWith(TransactionResolutionException::class) {
|
||||
tg.verify()
|
||||
}
|
||||
assertEquals(e.hash, input.txhash)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun duplicatedInputs() {
|
||||
// Check that a transaction cannot refer to the same input more than once.
|
||||
transactionGroup {
|
||||
roots {
|
||||
transaction(A_THOUSAND_POUNDS label "£1000")
|
||||
}
|
||||
|
||||
transaction {
|
||||
input("£1000")
|
||||
input("£1000")
|
||||
output { A_THOUSAND_POUNDS.copy(amount = A_THOUSAND_POUNDS.amount * 2) }
|
||||
arg(MINI_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
}
|
||||
|
||||
assertFailsWith(TransactionConflictException::class) {
|
||||
verify()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun signGroup() {
|
||||
val signedTxns: List<SignedTransaction> = transactionGroup {
|
||||
transaction {
|
||||
output("£1000") { A_THOUSAND_POUNDS }
|
||||
arg(MINI_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||
}
|
||||
|
||||
transaction {
|
||||
input("£1000")
|
||||
output("alice's £1000") { A_THOUSAND_POUNDS `owned by` ALICE_PUBKEY }
|
||||
arg(MINI_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
}
|
||||
|
||||
transaction {
|
||||
input("alice's £1000")
|
||||
arg(ALICE_PUBKEY) { Cash.Commands.Move() }
|
||||
arg(MINI_CORP_PUBKEY) { Cash.Commands.Exit(1000.POUNDS) }
|
||||
}
|
||||
}.signAll()
|
||||
|
||||
// Now go through the conversion -> verification path with them.
|
||||
val ltxns = signedTxns.map {
|
||||
it.verifyToLedgerTransaction(MockIdentityService, MockStorageService().attachments)
|
||||
}.toSet()
|
||||
TransactionGroup(ltxns, emptySet()).verify()
|
||||
}
|
||||
}
|
@ -1,258 +0,0 @@
|
||||
package core.node
|
||||
|
||||
import contracts.DUMMY_PROGRAM_ID
|
||||
import contracts.DummyContract
|
||||
import core.*
|
||||
import core.contracts.*
|
||||
import core.crypto.Party
|
||||
import core.crypto.SecureHash
|
||||
import core.node.services.AttachmentStorage
|
||||
import core.serialization.*
|
||||
import core.testutils.DUMMY_NOTARY
|
||||
import core.testutils.MEGA_CORP
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.junit.Test
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.net.URLClassLoader
|
||||
import java.util.jar.JarOutputStream
|
||||
import java.util.zip.ZipEntry
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertNotNull
|
||||
|
||||
interface DummyContractBackdoor {
|
||||
fun generateInitial(owner: PartyAndReference, magicNumber: Int, notary: Party): TransactionBuilder
|
||||
fun inspectState(state: ContractState): Int
|
||||
}
|
||||
|
||||
class AttachmentClassLoaderTests {
|
||||
companion object {
|
||||
val ISOLATED_CONTRACTS_JAR_PATH = AttachmentClassLoaderTests::class.java.getResource("isolated.jar")
|
||||
}
|
||||
|
||||
fun importJar(storage: AttachmentStorage) = ISOLATED_CONTRACTS_JAR_PATH.openStream().use { storage.importAttachment(it) }
|
||||
|
||||
// These ClassLoaders work together to load 'AnotherDummyContract' in a disposable way, such that even though
|
||||
// the class may be on the unit test class path (due to default IDE settings, etc), it won't be loaded into the
|
||||
// regular app classloader but rather than ClassLoaderForTests. This helps keep our environment clean and
|
||||
// ensures we have precise control over where it's loaded.
|
||||
object FilteringClassLoader : ClassLoader() {
|
||||
override fun loadClass(name: String, resolve: Boolean): Class<*>? {
|
||||
if ("AnotherDummyContract" in name) {
|
||||
return null
|
||||
} else
|
||||
return super.loadClass(name, resolve)
|
||||
}
|
||||
}
|
||||
|
||||
class ClassLoaderForTests : URLClassLoader(arrayOf(ISOLATED_CONTRACTS_JAR_PATH), FilteringClassLoader)
|
||||
|
||||
@Test
|
||||
fun `dynamically load AnotherDummyContract from isolated contracts jar`() {
|
||||
val child = ClassLoaderForTests()
|
||||
|
||||
val contractClass = Class.forName("contracts.isolated.AnotherDummyContract", true, child)
|
||||
val contract = contractClass.newInstance() as Contract
|
||||
|
||||
assertEquals(SecureHash.sha256("https://anotherdummy.org"), contract.legalContractReference)
|
||||
}
|
||||
|
||||
fun fakeAttachment(filepath: String, content: String): ByteArray {
|
||||
val bs = ByteArrayOutputStream()
|
||||
val js = JarOutputStream(bs)
|
||||
js.putNextEntry(ZipEntry(filepath))
|
||||
js.writer().apply { append(content); flush() }
|
||||
js.closeEntry()
|
||||
js.close()
|
||||
return bs.toByteArray()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test MockAttachmentStorage open as jar`() {
|
||||
val storage = MockAttachmentStorage()
|
||||
val key = importJar(storage)
|
||||
val attachment = storage.openAttachment(key)!!
|
||||
|
||||
val jar = attachment.openAsJAR()
|
||||
|
||||
assert(jar.nextEntry != null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test overlapping file exception`() {
|
||||
val storage = MockAttachmentStorage()
|
||||
|
||||
val att0 = importJar(storage)
|
||||
val att1 = storage.importAttachment(ByteArrayInputStream(fakeAttachment("file.txt", "some data")))
|
||||
val att2 = storage.importAttachment(ByteArrayInputStream(fakeAttachment("file.txt", "some other data")))
|
||||
|
||||
assertFailsWith(AttachmentsClassLoader.OverlappingAttachments::class) {
|
||||
AttachmentsClassLoader(arrayOf(att0, att1, att2).map { storage.openAttachment(it)!! })
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `basic`() {
|
||||
val storage = MockAttachmentStorage()
|
||||
|
||||
val att0 = importJar(storage)
|
||||
val att1 = storage.importAttachment(ByteArrayInputStream(fakeAttachment("file1.txt", "some data")))
|
||||
val att2 = storage.importAttachment(ByteArrayInputStream(fakeAttachment("file2.txt", "some other data")))
|
||||
|
||||
val cl = AttachmentsClassLoader(arrayOf(att0, att1, att2).map { storage.openAttachment(it)!! })
|
||||
val txt = IOUtils.toString(cl.getResourceAsStream("file1.txt"))
|
||||
assertEquals("some data", txt)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loading class AnotherDummyContract`() {
|
||||
val storage = MockAttachmentStorage()
|
||||
|
||||
val att0 = importJar(storage)
|
||||
val att1 = storage.importAttachment(ByteArrayInputStream(fakeAttachment("file1.txt", "some data")))
|
||||
val att2 = storage.importAttachment(ByteArrayInputStream(fakeAttachment("file2.txt", "some other data")))
|
||||
|
||||
val cl = AttachmentsClassLoader(arrayOf(att0, att1, att2).map { storage.openAttachment(it)!! }, FilteringClassLoader)
|
||||
val contractClass = Class.forName("contracts.isolated.AnotherDummyContract", true, cl)
|
||||
val contract = contractClass.newInstance() as Contract
|
||||
assertEquals(cl, contract.javaClass.classLoader)
|
||||
assertEquals(SecureHash.sha256("https://anotherdummy.org"), contract.legalContractReference)
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `verify that contract DummyContract is in classPath`() {
|
||||
val contractClass = Class.forName("contracts.DummyContract")
|
||||
val contract = contractClass.newInstance() as Contract
|
||||
|
||||
assertNotNull(contract)
|
||||
}
|
||||
|
||||
fun createContract2Cash(): Contract {
|
||||
val cl = ClassLoaderForTests()
|
||||
val contractClass = Class.forName("contracts.isolated.AnotherDummyContract", true, cl)
|
||||
return contractClass.newInstance() as Contract
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `testing Kryo with ClassLoader (with top level class name)`() {
|
||||
val contract = createContract2Cash()
|
||||
|
||||
val bytes = contract.serialize()
|
||||
|
||||
val storage = MockAttachmentStorage()
|
||||
|
||||
val att0 = importJar(storage)
|
||||
val att1 = storage.importAttachment(ByteArrayInputStream(fakeAttachment("file1.txt", "some data")))
|
||||
val att2 = storage.importAttachment(ByteArrayInputStream(fakeAttachment("file2.txt", "some other data")))
|
||||
|
||||
val cl = AttachmentsClassLoader(arrayOf(att0, att1, att2).map { storage.openAttachment(it)!! }, FilteringClassLoader)
|
||||
|
||||
val kryo = createKryo()
|
||||
kryo.classLoader = cl
|
||||
|
||||
val state2 = bytes.deserialize(kryo)
|
||||
assert(state2.javaClass.classLoader is AttachmentsClassLoader)
|
||||
assertNotNull(state2)
|
||||
}
|
||||
|
||||
// top level wrapper
|
||||
class Data(val contract: Contract)
|
||||
|
||||
@Test
|
||||
fun `testing Kryo with ClassLoader (without top level class name)`() {
|
||||
val data = Data(createContract2Cash())
|
||||
|
||||
assertNotNull(data.contract)
|
||||
|
||||
val bytes = data.serialize()
|
||||
|
||||
val storage = MockAttachmentStorage()
|
||||
|
||||
val att0 = importJar(storage)
|
||||
val att1 = storage.importAttachment(ByteArrayInputStream(fakeAttachment("file1.txt", "some data")))
|
||||
val att2 = storage.importAttachment(ByteArrayInputStream(fakeAttachment("file2.txt", "some other data")))
|
||||
|
||||
val cl = AttachmentsClassLoader(arrayOf(att0, att1, att2).map { storage.openAttachment(it)!! }, FilteringClassLoader)
|
||||
|
||||
val kryo = createKryo()
|
||||
kryo.classLoader = cl
|
||||
|
||||
val state2 = bytes.deserialize(kryo)
|
||||
assertEquals(cl, state2.contract.javaClass.classLoader)
|
||||
assertNotNull(state2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test serialization of WireTransaction with statically loaded contract`() {
|
||||
val tx = DUMMY_PROGRAM_ID.generateInitial(MEGA_CORP.ref(0), 42, DUMMY_NOTARY)
|
||||
val wireTransaction = tx.toWireTransaction()
|
||||
val bytes = wireTransaction.serialize()
|
||||
val copiedWireTransaction = bytes.deserialize()
|
||||
|
||||
assertEquals(1, copiedWireTransaction.outputs.size)
|
||||
assertEquals(42, (copiedWireTransaction.outputs[0] as DummyContract.State).magicNumber)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test serialization of WireTransaction with dynamically loaded contract`() {
|
||||
val child = ClassLoaderForTests()
|
||||
val contractClass = Class.forName("contracts.isolated.AnotherDummyContract", true, child)
|
||||
val contract = contractClass.newInstance() as DummyContractBackdoor
|
||||
val tx = contract.generateInitial(MEGA_CORP.ref(0), 42, DUMMY_NOTARY)
|
||||
val storage = MockAttachmentStorage()
|
||||
val kryo = createKryo()
|
||||
|
||||
// todo - think about better way to push attachmentStorage down to serializer
|
||||
kryo.attachmentStorage = storage
|
||||
|
||||
val attachmentRef = importJar(storage)
|
||||
|
||||
tx.addAttachment(storage.openAttachment(attachmentRef)!!)
|
||||
|
||||
val wireTransaction = tx.toWireTransaction()
|
||||
|
||||
val bytes = wireTransaction.serialize(kryo)
|
||||
|
||||
val kryo2 = createKryo()
|
||||
// use empty attachmentStorage
|
||||
kryo2.attachmentStorage = storage
|
||||
|
||||
val copiedWireTransaction = bytes.deserialize(kryo2)
|
||||
|
||||
assertEquals(1, copiedWireTransaction.outputs.size)
|
||||
val contract2 = copiedWireTransaction.outputs[0].contract as DummyContractBackdoor
|
||||
assertEquals(42, contract2.inspectState(copiedWireTransaction.outputs[0]))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test deserialize of WireTransaction where contract cannot be found`() {
|
||||
val child = ClassLoaderForTests()
|
||||
val contractClass = Class.forName("contracts.isolated.AnotherDummyContract", true, child)
|
||||
val contract = contractClass.newInstance() as DummyContractBackdoor
|
||||
val tx = contract.generateInitial(MEGA_CORP.ref(0), 42, DUMMY_NOTARY)
|
||||
val storage = MockAttachmentStorage()
|
||||
val kryo = createKryo()
|
||||
|
||||
// todo - think about better way to push attachmentStorage down to serializer
|
||||
kryo.attachmentStorage = storage
|
||||
|
||||
val attachmentRef = importJar(storage)
|
||||
|
||||
tx.addAttachment(storage.openAttachment(attachmentRef)!!)
|
||||
|
||||
val wireTransaction = tx.toWireTransaction()
|
||||
|
||||
val bytes = wireTransaction.serialize(kryo)
|
||||
|
||||
val kryo2 = createKryo()
|
||||
// use empty attachmentStorage
|
||||
kryo2.attachmentStorage = MockAttachmentStorage()
|
||||
|
||||
val e = assertFailsWith(MissingAttachmentsException::class) {
|
||||
bytes.deserialize(kryo2)
|
||||
}
|
||||
assertEquals(attachmentRef, e.ids.single())
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
package core.serialization
|
||||
|
||||
import com.esotericsoftware.kryo.Kryo
|
||||
import org.junit.Test
|
||||
import java.time.Instant
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class KryoTests {
|
||||
data class Person(val name: String, val birthday: Instant?)
|
||||
|
||||
private val kryo: Kryo = createKryo()
|
||||
|
||||
@Test
|
||||
fun ok() {
|
||||
val april_17th = Instant.parse("1984-04-17T00:30:00.00Z")
|
||||
val mike = Person("mike", april_17th)
|
||||
val bits = mike.serialize(kryo)
|
||||
with(bits.deserialize<Person>(kryo)) {
|
||||
assertEquals("mike", name)
|
||||
assertEquals(april_17th, birthday)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nullables() {
|
||||
val bob = Person("bob", null)
|
||||
val bits = bob.serialize(kryo)
|
||||
with(bits.deserialize<Person>(kryo)) {
|
||||
assertEquals("bob", name)
|
||||
assertNull(birthday)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,82 +0,0 @@
|
||||
package core.serialization
|
||||
|
||||
import contracts.Cash
|
||||
import core.*
|
||||
import core.contracts.*
|
||||
import core.testutils.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.security.SignatureException
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class TransactionSerializationTests {
|
||||
// Simple TX that takes 1000 pounds from me and sends 600 to someone else (with 400 change).
|
||||
// It refers to a fake TX/state that we don't bother creating here.
|
||||
val depositRef = MINI_CORP.ref(1)
|
||||
val outputState = Cash.State(depositRef, 600.POUNDS, DUMMY_PUBKEY_1, DUMMY_NOTARY)
|
||||
val changeState = Cash.State(depositRef, 400.POUNDS, TestUtils.keypair.public, DUMMY_NOTARY)
|
||||
|
||||
val fakeStateRef = generateStateRef()
|
||||
lateinit var tx: TransactionBuilder
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
tx = TransactionBuilder().withItems(
|
||||
fakeStateRef, outputState, changeState, Command(Cash.Commands.Move(), arrayListOf(TestUtils.keypair.public))
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun signWireTX() {
|
||||
tx.signWith(TestUtils.keypair)
|
||||
val signedTX = tx.toSignedTransaction()
|
||||
|
||||
// Now check that the signature we just made verifies.
|
||||
signedTX.verifySignatures()
|
||||
|
||||
// Corrupt the data and ensure the signature catches the problem.
|
||||
signedTX.txBits.bits[5] = 0
|
||||
assertFailsWith(SignatureException::class) {
|
||||
signedTX.verifySignatures()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun wrongKeys() {
|
||||
// Can't convert if we don't have signatures for all commands
|
||||
assertFailsWith(IllegalStateException::class) {
|
||||
tx.toSignedTransaction()
|
||||
}
|
||||
|
||||
tx.signWith(TestUtils.keypair)
|
||||
val signedTX = tx.toSignedTransaction()
|
||||
|
||||
// Cannot construct with an empty sigs list.
|
||||
assertFailsWith(IllegalStateException::class) {
|
||||
signedTX.copy(sigs = emptyList())
|
||||
}
|
||||
|
||||
// If the signature was replaced in transit, we don't like it.
|
||||
assertFailsWith(SignatureException::class) {
|
||||
val tx2 = TransactionBuilder().withItems(fakeStateRef, outputState, changeState,
|
||||
Command(Cash.Commands.Move(), TestUtils.keypair2.public))
|
||||
tx2.signWith(TestUtils.keypair2)
|
||||
|
||||
signedTX.copy(sigs = tx2.toSignedTransaction().sigs).verify()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun timestamp() {
|
||||
tx.setTime(TEST_TX_TIME, DUMMY_NOTARY, 30.seconds)
|
||||
tx.signWith(TestUtils.keypair)
|
||||
tx.signWith(DUMMY_NOTARY_KEY)
|
||||
val stx = tx.toSignedTransaction()
|
||||
val ltx = stx.verifyToLedgerTransaction(MockIdentityService, MockStorageService().attachments)
|
||||
assertEquals(tx.commands().map { it.value }, ltx.commands.map { it.value })
|
||||
assertEquals(tx.inputStates(), ltx.inputs)
|
||||
assertEquals(tx.outputStates(), ltx.outputs)
|
||||
assertEquals(TEST_TX_TIME, ltx.commands.getTimestampBy(DUMMY_NOTARY)!!.midpoint)
|
||||
}
|
||||
}
|
@ -1,387 +0,0 @@
|
||||
@file:Suppress("UNUSED_PARAMETER", "UNCHECKED_CAST")
|
||||
|
||||
package core.testutils
|
||||
|
||||
import com.google.common.base.Throwables
|
||||
import com.google.common.net.HostAndPort
|
||||
import contracts.*
|
||||
import core.*
|
||||
import core.contracts.*
|
||||
import core.crypto.*
|
||||
import core.node.AbstractNode
|
||||
import core.serialization.serialize
|
||||
import core.testing.MockIdentityService
|
||||
import core.visualiser.GraphVisualiser
|
||||
import java.net.ServerSocket
|
||||
import java.security.KeyPair
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.fail
|
||||
|
||||
/** If an exception is thrown by the body, rethrows the root cause exception. */
|
||||
inline fun <R> rootCauseExceptions(body: () -> R): R {
|
||||
try {
|
||||
return body()
|
||||
} catch(e: Exception) {
|
||||
throw Throwables.getRootCause(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun freeLocalHostAndPort(): HostAndPort {
|
||||
val freePort = ServerSocket(0).use { it.localPort }
|
||||
return HostAndPort.fromParts("localhost", freePort)
|
||||
}
|
||||
|
||||
object TestUtils {
|
||||
val keypair = generateKeyPair()
|
||||
val keypair2 = generateKeyPair()
|
||||
val keypair3 = generateKeyPair()
|
||||
}
|
||||
|
||||
// A dummy time at which we will be pretending test transactions are created.
|
||||
val TEST_TX_TIME = Instant.parse("2015-04-17T12:00:00.00Z")
|
||||
|
||||
// A few dummy values for testing.
|
||||
val MEGA_CORP_KEY = TestUtils.keypair
|
||||
val MEGA_CORP_PUBKEY = MEGA_CORP_KEY.public
|
||||
|
||||
val MINI_CORP_KEY = TestUtils.keypair2
|
||||
val MINI_CORP_PUBKEY = MINI_CORP_KEY.public
|
||||
|
||||
val ORACLE_KEY = TestUtils.keypair3
|
||||
val ORACLE_PUBKEY = ORACLE_KEY.public
|
||||
|
||||
val DUMMY_PUBKEY_1 = DummyPublicKey("x1")
|
||||
val DUMMY_PUBKEY_2 = DummyPublicKey("x2")
|
||||
|
||||
val ALICE_KEY = generateKeyPair()
|
||||
val ALICE_PUBKEY = ALICE_KEY.public
|
||||
val ALICE = Party("Alice", ALICE_PUBKEY)
|
||||
|
||||
val BOB_KEY = generateKeyPair()
|
||||
val BOB_PUBKEY = BOB_KEY.public
|
||||
val BOB = Party("Bob", BOB_PUBKEY)
|
||||
|
||||
val MEGA_CORP = Party("MegaCorp", MEGA_CORP_PUBKEY)
|
||||
val MINI_CORP = Party("MiniCorp", MINI_CORP_PUBKEY)
|
||||
|
||||
val DUMMY_NOTARY_KEY = generateKeyPair()
|
||||
val DUMMY_NOTARY = Party("Notary Service", DUMMY_NOTARY_KEY.public)
|
||||
|
||||
val ALL_TEST_KEYS = listOf(MEGA_CORP_KEY, MINI_CORP_KEY, ALICE_KEY, BOB_KEY, DUMMY_NOTARY_KEY)
|
||||
|
||||
val MockIdentityService = MockIdentityService(listOf(MEGA_CORP, MINI_CORP, DUMMY_NOTARY))
|
||||
|
||||
// In a real system this would be a persistent map of hash to bytecode and we'd instantiate the object as needed inside
|
||||
// a sandbox. For unit tests we just have a hard-coded list.
|
||||
val TEST_PROGRAM_MAP: Map<Contract, Class<out Contract>> = mapOf(
|
||||
CASH_PROGRAM_ID to Cash::class.java,
|
||||
CP_PROGRAM_ID to CommercialPaper::class.java,
|
||||
JavaCommercialPaper.JCP_PROGRAM_ID to JavaCommercialPaper::class.java,
|
||||
CROWDFUND_PROGRAM_ID to CrowdFund::class.java,
|
||||
DUMMY_PROGRAM_ID to DummyContract::class.java,
|
||||
IRS_PROGRAM_ID to InterestRateSwap::class.java
|
||||
)
|
||||
|
||||
fun generateState(notary: Party = DUMMY_NOTARY) = DummyContract.State(Random().nextInt(), notary)
|
||||
fun generateStateRef() = StateRef(SecureHash.randomSHA256(), 0)
|
||||
|
||||
fun issueState(node: AbstractNode, notary: Party = DUMMY_NOTARY): StateRef {
|
||||
val tx = DummyContract().generateInitial(node.info.identity.ref(0), Random().nextInt(), DUMMY_NOTARY)
|
||||
tx.signWith(node.storage.myLegalIdentityKey)
|
||||
tx.signWith(DUMMY_NOTARY_KEY)
|
||||
val stx = tx.toSignedTransaction()
|
||||
node.services.recordTransactions(listOf(stx))
|
||||
return StateRef(stx.id, 0)
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Defines a simple DSL for building pseudo-transactions (not the same as the wire protocol) for testing purposes.
|
||||
//
|
||||
// Define a transaction like this:
|
||||
//
|
||||
// transaction {
|
||||
// input { someExpression }
|
||||
// output { someExpression }
|
||||
// arg { someExpression }
|
||||
//
|
||||
// tweak {
|
||||
// ... same thing but works with a copy of the parent, can add inputs/outputs/args just within this scope.
|
||||
// }
|
||||
//
|
||||
// contract.accepts() -> should pass
|
||||
// contract `fails requirement` "some substring of the error message"
|
||||
// }
|
||||
//
|
||||
// TODO: Make it impossible to forget to test either a failure or an accept for each transaction{} block
|
||||
|
||||
infix fun Cash.State.`owned by`(owner: PublicKey) = copy(owner = owner)
|
||||
infix fun Cash.State.`issued by`(party: Party) = copy(deposit = deposit.copy(party = party))
|
||||
infix fun CommercialPaper.State.`owned by`(owner: PublicKey) = this.copy(owner = owner)
|
||||
infix fun ICommercialPaperState.`owned by`(new_owner: PublicKey) = this.withOwner(new_owner)
|
||||
|
||||
// Allows you to write 100.DOLLARS.CASH
|
||||
val Amount.CASH: Cash.State get() = Cash.State(MINI_CORP.ref(1, 2, 3), this, NullPublicKey, DUMMY_NOTARY)
|
||||
|
||||
class LabeledOutput(val label: String?, val state: ContractState) {
|
||||
override fun toString() = state.toString() + (if (label != null) " ($label)" else "")
|
||||
override fun equals(other: Any?) = other is LabeledOutput && state.equals(other.state)
|
||||
override fun hashCode(): Int = state.hashCode()
|
||||
}
|
||||
|
||||
infix fun ContractState.label(label: String) = LabeledOutput(label, this)
|
||||
|
||||
abstract class AbstractTransactionForTest {
|
||||
protected val attachments = ArrayList<SecureHash>()
|
||||
protected val outStates = ArrayList<LabeledOutput>()
|
||||
protected val commands = ArrayList<Command>()
|
||||
|
||||
open fun output(label: String? = null, s: () -> ContractState) = LabeledOutput(label, s()).apply { outStates.add(this) }
|
||||
|
||||
protected fun commandsToAuthenticatedObjects(): List<AuthenticatedObject<CommandData>> {
|
||||
return commands.map { AuthenticatedObject(it.signers, it.signers.mapNotNull { MockIdentityService.partyFromKey(it) }, it.value) }
|
||||
}
|
||||
|
||||
fun attachment(attachmentID: SecureHash) {
|
||||
attachments.add(attachmentID)
|
||||
}
|
||||
|
||||
fun arg(vararg key: PublicKey, c: () -> CommandData) {
|
||||
val keys = listOf(*key)
|
||||
commands.add(Command(c(), keys))
|
||||
}
|
||||
|
||||
fun timestamp(time: Instant) {
|
||||
val data = TimestampCommand(time, 30.seconds)
|
||||
timestamp(data)
|
||||
}
|
||||
|
||||
fun timestamp(data: TimestampCommand) {
|
||||
commands.add(Command(data, DUMMY_NOTARY.owningKey))
|
||||
}
|
||||
|
||||
// Forbid patterns like: transaction { ... transaction { ... } }
|
||||
@Deprecated("Cannot nest transactions, use tweak", level = DeprecationLevel.ERROR)
|
||||
fun transaction(body: TransactionForTest.() -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
// Corresponds to the args to Contract.verify
|
||||
open class TransactionForTest : AbstractTransactionForTest() {
|
||||
private val inStates = arrayListOf<ContractState>()
|
||||
fun input(s: () -> ContractState) = inStates.add(s())
|
||||
|
||||
protected fun runCommandsAndVerify(time: Instant) {
|
||||
val cmds = commandsToAuthenticatedObjects()
|
||||
val tx = TransactionForVerification(inStates, outStates.map { it.state }, emptyList(), cmds, SecureHash.randomSHA256())
|
||||
tx.verify()
|
||||
}
|
||||
|
||||
fun accepts(time: Instant = TEST_TX_TIME) = runCommandsAndVerify(time)
|
||||
fun rejects(withMessage: String? = null, time: Instant = TEST_TX_TIME) {
|
||||
val r = try {
|
||||
runCommandsAndVerify(time)
|
||||
false
|
||||
} catch (e: Exception) {
|
||||
val m = e.message
|
||||
if (m == null)
|
||||
fail("Threw exception without a message")
|
||||
else
|
||||
if (withMessage != null && !m.toLowerCase().contains(withMessage.toLowerCase())) throw AssertionError("Error was actually: $m", e)
|
||||
true
|
||||
}
|
||||
if (!r) throw AssertionError("Expected exception but didn't get one")
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to confirm that the test, when (implicitly) run against the .verify() method, fails with the text of the message
|
||||
*/
|
||||
infix fun `fails requirement`(msg: String) = rejects(msg)
|
||||
|
||||
fun fails_requirement(msg: String) = this.`fails requirement`(msg)
|
||||
|
||||
// Use this to create transactions where the output of this transaction is automatically used as an input of
|
||||
// the next.
|
||||
fun chain(vararg outputLabels: String, body: TransactionForTest.() -> Unit): TransactionForTest {
|
||||
val states = outStates.mapNotNull {
|
||||
val l = it.label
|
||||
if (l != null && outputLabels.contains(l))
|
||||
it.state
|
||||
else
|
||||
null
|
||||
}
|
||||
val tx = TransactionForTest()
|
||||
tx.inStates.addAll(states)
|
||||
tx.body()
|
||||
return tx
|
||||
}
|
||||
|
||||
// Allow customisation of partial transactions.
|
||||
fun tweak(body: TransactionForTest.() -> Unit): TransactionForTest {
|
||||
val tx = TransactionForTest()
|
||||
tx.inStates.addAll(inStates)
|
||||
tx.outStates.addAll(outStates)
|
||||
tx.commands.addAll(commands)
|
||||
tx.body()
|
||||
return tx
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return """transaction {
|
||||
inputs: $inStates
|
||||
outputs: $outStates
|
||||
commands $commands
|
||||
}"""
|
||||
}
|
||||
|
||||
override fun equals(other: Any?) = this === other || (other is TransactionForTest && inStates == other.inStates && outStates == other.outStates && commands == other.commands)
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = inStates.hashCode()
|
||||
result += 31 * result + outStates.hashCode()
|
||||
result += 31 * result + commands.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
fun transaction(body: TransactionForTest.() -> Unit) = TransactionForTest().apply { body() }
|
||||
|
||||
class TransactionGroupDSL<T : ContractState>(private val stateType: Class<T>) {
|
||||
open inner class WireTransactionDSL : AbstractTransactionForTest() {
|
||||
private val inStates = ArrayList<StateRef>()
|
||||
|
||||
fun input(label: String) {
|
||||
inStates.add(label.outputRef)
|
||||
}
|
||||
|
||||
fun toWireTransaction() = WireTransaction(inStates, attachments, outStates.map { it.state }, commands)
|
||||
}
|
||||
|
||||
val String.output: T get() = labelToOutputs[this] ?: throw IllegalArgumentException("State with label '$this' was not found")
|
||||
val String.outputRef: StateRef get() = labelToRefs[this] ?: throw IllegalArgumentException("Unknown label \"$this\"")
|
||||
|
||||
fun <C : ContractState> lookup(label: String) = StateAndRef(label.output as C, label.outputRef)
|
||||
|
||||
private inner class InternalWireTransactionDSL : WireTransactionDSL() {
|
||||
fun finaliseAndInsertLabels(): WireTransaction {
|
||||
val wtx = toWireTransaction()
|
||||
for ((index, labelledState) in outStates.withIndex()) {
|
||||
if (labelledState.label != null) {
|
||||
labelToRefs[labelledState.label] = StateRef(wtx.id, index)
|
||||
if (stateType.isInstance(labelledState.state)) {
|
||||
labelToOutputs[labelledState.label] = labelledState.state as T
|
||||
}
|
||||
outputsToLabels[labelledState.state] = labelledState.label
|
||||
}
|
||||
}
|
||||
return wtx
|
||||
}
|
||||
}
|
||||
|
||||
private val rootTxns = ArrayList<WireTransaction>()
|
||||
private val labelToRefs = HashMap<String, StateRef>()
|
||||
private val labelToOutputs = HashMap<String, T>()
|
||||
private val outputsToLabels = HashMap<ContractState, String>()
|
||||
|
||||
fun labelForState(state: T): String? = outputsToLabels[state]
|
||||
|
||||
inner class Roots {
|
||||
fun transaction(vararg outputStates: LabeledOutput) {
|
||||
val outs = outputStates.map { it.state }
|
||||
val wtx = WireTransaction(emptyList(), emptyList(), outs, emptyList())
|
||||
for ((index, state) in outputStates.withIndex()) {
|
||||
val label = state.label!!
|
||||
labelToRefs[label] = StateRef(wtx.id, index)
|
||||
outputsToLabels[state.state] = label
|
||||
labelToOutputs[label] = state.state as T
|
||||
}
|
||||
rootTxns.add(wtx)
|
||||
}
|
||||
|
||||
@Deprecated("Does not nest ", level = DeprecationLevel.ERROR)
|
||||
fun roots(body: Roots.() -> Unit) {
|
||||
}
|
||||
|
||||
@Deprecated("Use the vararg form of transaction inside roots", level = DeprecationLevel.ERROR)
|
||||
fun transaction(body: WireTransactionDSL.() -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
fun roots(body: Roots.() -> Unit) = Roots().apply { body() }
|
||||
|
||||
val txns = ArrayList<WireTransaction>()
|
||||
private val txnToLabelMap = HashMap<SecureHash, String>()
|
||||
|
||||
fun transaction(label: String? = null, body: WireTransactionDSL.() -> Unit): WireTransaction {
|
||||
val forTest = InternalWireTransactionDSL()
|
||||
forTest.body()
|
||||
val wtx = forTest.finaliseAndInsertLabels()
|
||||
txns.add(wtx)
|
||||
if (label != null)
|
||||
txnToLabelMap[wtx.id] = label
|
||||
return wtx
|
||||
}
|
||||
|
||||
fun labelForTransaction(tx: WireTransaction): String? = txnToLabelMap[tx.id]
|
||||
fun labelForTransaction(tx: LedgerTransaction): String? = txnToLabelMap[tx.id]
|
||||
|
||||
@Deprecated("Does not nest ", level = DeprecationLevel.ERROR)
|
||||
fun transactionGroup(body: TransactionGroupDSL<T>.() -> Unit) {
|
||||
}
|
||||
|
||||
fun toTransactionGroup() = TransactionGroup(
|
||||
txns.map { it.toLedgerTransaction(MockIdentityService, MockStorageService().attachments) }.toSet(),
|
||||
rootTxns.map { it.toLedgerTransaction(MockIdentityService, MockStorageService().attachments) }.toSet()
|
||||
)
|
||||
|
||||
class Failed(val index: Int, cause: Throwable) : Exception("Transaction $index didn't verify", cause)
|
||||
|
||||
fun verify() {
|
||||
val group = toTransactionGroup()
|
||||
try {
|
||||
group.verify()
|
||||
} catch (e: TransactionVerificationException) {
|
||||
// Let the developer know the index of the transaction that failed.
|
||||
val wtx: WireTransaction = txns.find { it.id == e.tx.origHash }!!
|
||||
throw Failed(txns.indexOf(wtx) + 1, e)
|
||||
}
|
||||
}
|
||||
|
||||
fun expectFailureOfTx(index: Int, message: String): Exception {
|
||||
val e = assertFailsWith(Failed::class) {
|
||||
verify()
|
||||
}
|
||||
assertEquals(index, e.index)
|
||||
if (!e.cause!!.message!!.contains(message))
|
||||
throw AssertionError("Exception should have said '$message' but was actually: ${e.cause.message}", e.cause)
|
||||
return e
|
||||
}
|
||||
|
||||
fun visualise() {
|
||||
@Suppress("CAST_NEVER_SUCCEEDS")
|
||||
GraphVisualiser(this as TransactionGroupDSL<ContractState>).display()
|
||||
}
|
||||
|
||||
fun signAll(txnsToSign: List<WireTransaction> = txns, vararg extraKeys: KeyPair): List<SignedTransaction> {
|
||||
return txnsToSign.map { wtx ->
|
||||
val allPubKeys = wtx.commands.flatMap { it.signers }.toMutableSet()
|
||||
val bits = wtx.serialize()
|
||||
require(bits == wtx.serialized)
|
||||
val sigs = ArrayList<DigitalSignature.WithKey>()
|
||||
for (key in ALL_TEST_KEYS + extraKeys) {
|
||||
if (allPubKeys.contains(key.public)) {
|
||||
sigs += key.signWithECDSA(bits)
|
||||
allPubKeys -= key.public
|
||||
}
|
||||
}
|
||||
SignedTransaction(bits, sigs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified T : ContractState> transactionGroupFor(body: TransactionGroupDSL<T>.() -> Unit) = TransactionGroupDSL<T>(T::class.java).apply { this.body() }
|
||||
fun transactionGroup(body: TransactionGroupDSL<ContractState>.() -> Unit) = TransactionGroupDSL(ContractState::class.java).apply { this.body() }
|
@ -1,42 +0,0 @@
|
||||
package core.utilities
|
||||
|
||||
import core.indexOfOrThrow
|
||||
import core.noneOrSingle
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class CollectionExtensionTests {
|
||||
@Test
|
||||
fun `noneOrSingle returns a single item`() {
|
||||
val collection = listOf(1)
|
||||
assertEquals(collection.noneOrSingle(), 1)
|
||||
assertEquals(collection.noneOrSingle { it == 1 }, 1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `noneOrSingle returns null if item not found`() {
|
||||
val collection = emptyList<Int>()
|
||||
assertEquals(collection.noneOrSingle(), null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `noneOrSingle throws if more than one item found`() {
|
||||
val collection = listOf(1, 2)
|
||||
assertFailsWith<IllegalArgumentException> { collection.noneOrSingle() }
|
||||
assertFailsWith<IllegalArgumentException> { collection.noneOrSingle { it > 0 } }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `indexOfOrThrow returns index of the given item`() {
|
||||
val collection = listOf(1, 2)
|
||||
assertEquals(collection.indexOfOrThrow(1), 0)
|
||||
assertEquals(collection.indexOfOrThrow(2), 1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `indexOfOrThrow throws if the given item is not found`() {
|
||||
val collection = listOf(1)
|
||||
assertFailsWith<IllegalArgumentException> { collection.indexOfOrThrow(2) }
|
||||
}
|
||||
}
|
@ -1,18 +1,18 @@
|
||||
package core.messaging
|
||||
package node.messaging
|
||||
|
||||
import core.contracts.Attachment
|
||||
import core.crypto.SecureHash
|
||||
import core.crypto.sha256
|
||||
import core.node.NodeConfiguration
|
||||
import core.node.NodeInfo
|
||||
import core.node.services.NetworkMapService
|
||||
import core.node.services.NodeAttachmentService
|
||||
import core.node.services.NotaryService
|
||||
import core.node.services.ServiceType
|
||||
import core.serialization.OpaqueBytes
|
||||
import core.testing.MockNetwork
|
||||
import core.testutils.rootCauseExceptions
|
||||
import core.testing.rootCauseExceptions
|
||||
import core.utilities.BriefLogFormatter
|
||||
import node.core.testing.MockNetwork
|
||||
import node.services.config.NodeConfiguration
|
||||
import node.services.network.NetworkMapService
|
||||
import node.services.persistence.NodeAttachmentService
|
||||
import node.services.transactions.NotaryService
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import protocols.FetchAttachmentsProtocol
|
@ -1,9 +1,12 @@
|
||||
@file:Suppress("UNUSED_VARIABLE")
|
||||
|
||||
package core.messaging
|
||||
package node.messaging
|
||||
|
||||
import core.messaging.Message
|
||||
import core.messaging.TopicStringValidator
|
||||
import core.messaging.send
|
||||
import core.serialization.deserialize
|
||||
import core.testing.MockNetwork
|
||||
import node.core.testing.MockNetwork
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.util.*
|
@ -1,28 +1,33 @@
|
||||
package core.messaging
|
||||
package node.messaging
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import contracts.Cash
|
||||
import contracts.CommercialPaper
|
||||
import contracts.testing.CASH
|
||||
import contracts.testing.`issued by`
|
||||
import contracts.testing.`owned by`
|
||||
import core.contracts.*
|
||||
import core.crypto.Party
|
||||
import core.crypto.SecureHash
|
||||
import core.days
|
||||
import core.node.NodeConfiguration
|
||||
import core.messaging.SingleMessageRecipient
|
||||
import core.node.NodeInfo
|
||||
import core.node.ServiceHub
|
||||
import core.node.services.NodeAttachmentService
|
||||
import core.node.services.ServiceType
|
||||
import core.node.subsystems.NodeWalletService
|
||||
import core.node.subsystems.StorageServiceImpl
|
||||
import core.node.subsystems.Wallet
|
||||
import core.node.subsystems.WalletImpl
|
||||
import core.node.services.Wallet
|
||||
import core.random63BitValue
|
||||
import core.seconds
|
||||
import core.testing.InMemoryMessagingNetwork
|
||||
import core.testing.MockNetwork
|
||||
import core.testutils.*
|
||||
import core.testing.*
|
||||
import core.utilities.BriefLogFormatter
|
||||
import core.utilities.RecordingMap
|
||||
import node.core.testing.MockNetwork
|
||||
import node.services.config.NodeConfiguration
|
||||
import node.services.network.InMemoryMessagingNetwork
|
||||
import node.services.persistence.NodeAttachmentService
|
||||
import node.services.persistence.StorageServiceImpl
|
||||
import node.services.statemachine.StateMachineManager
|
||||
import node.services.wallet.NodeWalletService
|
||||
import node.services.wallet.WalletImpl
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
@ -50,10 +55,24 @@ import kotlin.test.assertTrue
|
||||
class TwoPartyTradeProtocolTests {
|
||||
lateinit var net: MockNetwork
|
||||
|
||||
private fun runSeller(smm: StateMachineManager, notary: NodeInfo,
|
||||
otherSide: SingleMessageRecipient, assetToSell: StateAndRef<OwnableState>, price: Amount,
|
||||
myKeyPair: KeyPair, buyerSessionID: Long): ListenableFuture<SignedTransaction> {
|
||||
val seller = TwoPartyTradeProtocol.Seller(otherSide, notary, assetToSell, price, myKeyPair, buyerSessionID)
|
||||
return smm.add("${TwoPartyTradeProtocol.TRADE_TOPIC}.seller", seller)
|
||||
}
|
||||
|
||||
private fun runBuyer(smm: StateMachineManager, notaryNode: NodeInfo,
|
||||
otherSide: SingleMessageRecipient, acceptablePrice: Amount, typeToBuy: Class<out OwnableState>,
|
||||
sessionID: Long): ListenableFuture<SignedTransaction> {
|
||||
val buyer = TwoPartyTradeProtocol.Buyer(otherSide, notaryNode.identity, acceptablePrice, typeToBuy, sessionID)
|
||||
return smm.add("${TwoPartyTradeProtocol.TRADE_TOPIC}.buyer", buyer)
|
||||
}
|
||||
|
||||
@Before
|
||||
fun before() {
|
||||
net = MockNetwork(false)
|
||||
net.identities += MockIdentityService.identities
|
||||
net.identities += MOCK_IDENTITY_SERVICE.identities
|
||||
BriefLogFormatter.loggingOn("platform.trade", "core.contract.TransactionGroup", "recordingmap")
|
||||
}
|
||||
|
||||
@ -113,7 +132,7 @@ class TwoPartyTradeProtocolTests {
|
||||
|
||||
@Test
|
||||
fun `shutdown and restore`() {
|
||||
transactionGroupFor<ContractState> {
|
||||
core.testing.transactionGroupFor<ContractState> {
|
||||
val notaryNode = net.createNotaryNode(DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
|
||||
val aliceNode = net.createPartyNode(notaryNode.info, ALICE.name, ALICE_KEY)
|
||||
var bobNode = net.createPartyNode(notaryNode.info, BOB.name, BOB_KEY)
|
||||
@ -219,22 +238,6 @@ class TwoPartyTradeProtocolTests {
|
||||
}, true, name, keyPair)
|
||||
}
|
||||
|
||||
|
||||
private fun runSeller(smm: StateMachineManager, notary: NodeInfo,
|
||||
otherSide: SingleMessageRecipient, assetToSell: StateAndRef<OwnableState>, price: Amount,
|
||||
myKeyPair: KeyPair, buyerSessionID: Long): ListenableFuture<SignedTransaction> {
|
||||
val seller = TwoPartyTradeProtocol.Seller(otherSide, notary, assetToSell, price, myKeyPair, buyerSessionID)
|
||||
return smm.add("${TwoPartyTradeProtocol.TRADE_TOPIC}.seller", seller)
|
||||
}
|
||||
|
||||
private fun runBuyer(smm: StateMachineManager, notaryNode: NodeInfo,
|
||||
otherSide: SingleMessageRecipient, acceptablePrice: Amount, typeToBuy: Class<out OwnableState>,
|
||||
sessionID: Long): ListenableFuture<SignedTransaction> {
|
||||
val buyer = TwoPartyTradeProtocol.Buyer(otherSide, notaryNode.identity, acceptablePrice, typeToBuy, sessionID)
|
||||
return smm.add("${TwoPartyTradeProtocol.TRADE_TOPIC}.buyer", buyer)
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun checkDependenciesOfSaleAssetAreResolved() {
|
||||
transactionGroupFor<ContractState> {
|
@ -1,8 +1,9 @@
|
||||
package core.node.subsystems
|
||||
package node.services
|
||||
|
||||
import core.messaging.Message
|
||||
import core.messaging.MessageRecipients
|
||||
import core.testutils.freeLocalHostAndPort
|
||||
import core.testing.freeLocalHostAndPort
|
||||
import node.services.messaging.ArtemisMessagingService
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
@ -1,6 +1,6 @@
|
||||
package core.node.subsystems
|
||||
package node.services
|
||||
|
||||
import core.testing.MockNetwork
|
||||
import node.core.testing.MockNetwork
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
@ -1,15 +1,16 @@
|
||||
package core.node.services
|
||||
package node.services
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import core.*
|
||||
import core.crypto.SecureHash
|
||||
import core.crypto.signWithECDSA
|
||||
import core.node.NodeInfo
|
||||
import core.protocols.ProtocolLogic
|
||||
import core.serialization.serialize
|
||||
import core.testing.MockNetwork
|
||||
import core.utilities.AddOrRemove
|
||||
import core.random63BitValue
|
||||
import core.utilities.BriefLogFormatter
|
||||
import node.core.testing.MockNetwork
|
||||
import node.services.network.InMemoryNetworkMapService
|
||||
import node.services.network.NetworkMapService
|
||||
import node.services.network.NodeRegistration
|
||||
import node.utilities.AddOrRemove
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.security.PrivateKey
|
69
node/src/test/kotlin/node/services/MockServices.kt
Normal file
69
node/src/test/kotlin/node/services/MockServices.kt
Normal file
@ -0,0 +1,69 @@
|
||||
package node.services
|
||||
|
||||
import com.codahale.metrics.MetricRegistry
|
||||
import core.messaging.MessagingService
|
||||
import core.node.services.*
|
||||
import core.node.services.testing.MockStorageService
|
||||
import core.testing.MOCK_IDENTITY_SERVICE
|
||||
import node.services.api.Checkpoint
|
||||
import node.services.api.CheckpointStorage
|
||||
import node.services.api.MonitoringService
|
||||
import node.services.api.ServiceHubInternal
|
||||
import node.services.network.MockNetworkMapCache
|
||||
import node.services.network.NetworkMapService
|
||||
import node.services.persistence.DataVendingService
|
||||
import node.services.wallet.NodeWalletService
|
||||
import java.time.Clock
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
|
||||
class MockCheckpointStorage : CheckpointStorage {
|
||||
|
||||
private val _checkpoints = ConcurrentLinkedQueue<Checkpoint>()
|
||||
override val checkpoints: Iterable<Checkpoint>
|
||||
get() = _checkpoints.toList()
|
||||
|
||||
override fun addCheckpoint(checkpoint: Checkpoint) {
|
||||
_checkpoints.add(checkpoint)
|
||||
}
|
||||
|
||||
override fun removeCheckpoint(checkpoint: Checkpoint) {
|
||||
require(_checkpoints.remove(checkpoint))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class MockServices(
|
||||
customWallet: WalletService? = null,
|
||||
val keyManagement: KeyManagementService? = null,
|
||||
val net: MessagingService? = null,
|
||||
val identity: IdentityService? = MOCK_IDENTITY_SERVICE,
|
||||
val storage: StorageService? = MockStorageService(),
|
||||
val mapCache: NetworkMapCache? = MockNetworkMapCache(),
|
||||
val mapService: NetworkMapService? = null,
|
||||
val overrideClock: Clock? = Clock.systemUTC()
|
||||
) : ServiceHubInternal {
|
||||
override val walletService: WalletService = customWallet ?: NodeWalletService(this)
|
||||
|
||||
override val keyManagementService: KeyManagementService
|
||||
get() = keyManagement ?: throw UnsupportedOperationException()
|
||||
override val identityService: IdentityService
|
||||
get() = identity ?: throw UnsupportedOperationException()
|
||||
override val networkService: MessagingService
|
||||
get() = net ?: throw UnsupportedOperationException()
|
||||
override val networkMapCache: NetworkMapCache
|
||||
get() = mapCache ?: throw UnsupportedOperationException()
|
||||
override val storageService: StorageService
|
||||
get() = storage ?: throw UnsupportedOperationException()
|
||||
override val clock: Clock
|
||||
get() = overrideClock ?: throw UnsupportedOperationException()
|
||||
|
||||
override val monitoringService: MonitoringService = MonitoringService(MetricRegistry())
|
||||
|
||||
init {
|
||||
if (net != null && storage != null) {
|
||||
// Creating this class is sufficient, we don't have to store it anywhere, because it registers a listener
|
||||
// on the networking service, so that will keep it from being collected.
|
||||
DataVendingService(net, storage)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,14 +1,15 @@
|
||||
package core.node
|
||||
package node.services
|
||||
|
||||
import com.codahale.metrics.MetricRegistry
|
||||
import com.google.common.jimfs.Configuration
|
||||
import com.google.common.jimfs.Jimfs
|
||||
import core.crypto.SecureHash
|
||||
import core.node.services.NodeAttachmentService
|
||||
import core.use
|
||||
import node.services.persistence.NodeAttachmentService
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.nio.charset.Charset
|
||||
import java.nio.file.FileAlreadyExistsException
|
||||
import java.nio.file.FileSystem
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
@ -52,7 +53,7 @@ class NodeAttachmentStorageTest {
|
||||
val testJar = makeTestJar()
|
||||
val storage = NodeAttachmentService(fs.getPath("/"), MetricRegistry())
|
||||
testJar.use { storage.importAttachment(it) }
|
||||
assertFailsWith<java.nio.file.FileAlreadyExistsException> {
|
||||
assertFailsWith<FileAlreadyExistsException> {
|
||||
testJar.use { storage.importAttachment(it) }
|
||||
}
|
||||
}
|
@ -1,13 +1,18 @@
|
||||
package core.node.services
|
||||
package node.services
|
||||
|
||||
import contracts.Cash
|
||||
import contracts.testing.CASH
|
||||
import contracts.testing.`owned by`
|
||||
import core.bd
|
||||
import core.contracts.DOLLARS
|
||||
import core.contracts.Fix
|
||||
import core.contracts.TransactionBuilder
|
||||
import core.bd
|
||||
import core.testing.MockNetwork
|
||||
import core.testutils.*
|
||||
import core.testing.ALICE_PUBKEY
|
||||
import core.testing.MEGA_CORP
|
||||
import core.testing.MEGA_CORP_KEY
|
||||
import core.utilities.BriefLogFormatter
|
||||
import node.core.testing.MockNetwork
|
||||
import node.services.clientapi.NodeInterestRates
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import protocols.RatesFixProtocol
|
@ -1,14 +1,16 @@
|
||||
package core.node.subsystems
|
||||
package node.services
|
||||
|
||||
import contracts.Cash
|
||||
import core.*
|
||||
import core.contracts.DOLLARS
|
||||
import core.contracts.TransactionBuilder
|
||||
import core.contracts.USD
|
||||
import core.contracts.verifyToLedgerTransaction
|
||||
import core.node.ServiceHub
|
||||
import core.testutils.*
|
||||
import core.node.services.testing.MockKeyManagementService
|
||||
import core.node.services.testing.MockStorageService
|
||||
import core.testing.*
|
||||
import core.utilities.BriefLogFormatter
|
||||
import node.services.wallet.NodeWalletService
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
@ -65,7 +67,7 @@ class NodeWalletServiceTest {
|
||||
Cash().generateIssue(this, 100.DOLLARS, MEGA_CORP.ref(1), freshKey.public, DUMMY_NOTARY)
|
||||
signWith(MEGA_CORP_KEY)
|
||||
}.toSignedTransaction()
|
||||
val myOutput = usefulTX.verifyToLedgerTransaction(MockIdentityService, MockStorageService().attachments).outRef<Cash.State>(0)
|
||||
val myOutput = usefulTX.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, MockStorageService().attachments).outRef<Cash.State>(0)
|
||||
|
||||
// A tx that spends our money.
|
||||
val spendTX = TransactionBuilder().apply {
|
@ -1,11 +1,11 @@
|
||||
package core.node.services
|
||||
package node.services
|
||||
|
||||
import core.contracts.TransactionBuilder
|
||||
import core.seconds
|
||||
import core.testing.MockNetwork
|
||||
import core.testutils.DUMMY_NOTARY
|
||||
import core.testutils.DUMMY_NOTARY_KEY
|
||||
import core.testutils.issueState
|
||||
import core.testing.DUMMY_NOTARY
|
||||
import core.testing.DUMMY_NOTARY_KEY
|
||||
import node.core.testing.MockNetwork
|
||||
import node.testutils.issueState
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import protocols.NotaryError
|
||||
@ -37,7 +37,7 @@ class NotaryServiceTests {
|
||||
tx.setTime(Instant.now(), DUMMY_NOTARY, 30.seconds)
|
||||
var wtx = tx.toWireTransaction()
|
||||
|
||||
val protocol = NotaryProtocol(wtx, NotaryProtocol.tracker())
|
||||
val protocol = NotaryProtocol(wtx, NotaryProtocol.Companion.tracker())
|
||||
val future = clientNode.smm.add(NotaryProtocol.TOPIC, protocol)
|
||||
net.runNetwork()
|
||||
|
||||
@ -49,7 +49,7 @@ class NotaryServiceTests {
|
||||
val inputState = issueState(clientNode)
|
||||
val wtx = TransactionBuilder().withItems(inputState).toWireTransaction()
|
||||
|
||||
val protocol = NotaryProtocol(wtx, NotaryProtocol.tracker())
|
||||
val protocol = NotaryProtocol(wtx, NotaryProtocol.Companion.tracker())
|
||||
val future = clientNode.smm.add(NotaryProtocol.TOPIC, protocol)
|
||||
net.runNetwork()
|
||||
|
||||
@ -63,7 +63,7 @@ class NotaryServiceTests {
|
||||
tx.setTime(Instant.now().plusSeconds(3600), DUMMY_NOTARY, 30.seconds)
|
||||
var wtx = tx.toWireTransaction()
|
||||
|
||||
val protocol = NotaryProtocol(wtx, NotaryProtocol.tracker())
|
||||
val protocol = NotaryProtocol(wtx, NotaryProtocol.Companion.tracker())
|
||||
val future = clientNode.smm.add(NotaryProtocol.TOPIC, protocol)
|
||||
net.runNetwork()
|
||||
|
@ -1,9 +1,11 @@
|
||||
package core.node.storage
|
||||
package node.services
|
||||
|
||||
import com.google.common.jimfs.Configuration.unix
|
||||
import com.google.common.jimfs.Jimfs
|
||||
import com.google.common.primitives.Ints
|
||||
import core.serialization.SerializedBytes
|
||||
import node.services.api.Checkpoint
|
||||
import node.services.persistence.PerFileCheckpointStorage
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatExceptionOfType
|
||||
import org.junit.After
|
@ -1,7 +1,8 @@
|
||||
package core.node.services
|
||||
package node.services
|
||||
|
||||
import core.contracts.TimestampCommand
|
||||
import core.seconds
|
||||
import node.services.transactions.TimestampChecker
|
||||
import org.junit.Test
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
@ -1,8 +1,10 @@
|
||||
package core.node.services
|
||||
package node.services
|
||||
|
||||
import core.contracts.TransactionBuilder
|
||||
import core.testutils.MEGA_CORP
|
||||
import core.testutils.generateStateRef
|
||||
import core.node.services.UniquenessException
|
||||
import core.testing.MEGA_CORP
|
||||
import core.testing.generateStateRef
|
||||
import node.services.transactions.InMemoryUniquenessProvider
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
@ -1,4 +1,4 @@
|
||||
package core.utilities
|
||||
package node.utilities
|
||||
|
||||
import org.junit.After
|
||||
import org.junit.Test
|
@ -1,4 +1,4 @@
|
||||
package core.visualiser
|
||||
package node.visualiser
|
||||
|
||||
import org.graphstream.graph.Edge
|
||||
import org.graphstream.graph.Element
|
||||
@ -42,7 +42,7 @@ fun createGraph(name: String, styles: String): SingleGraph {
|
||||
}
|
||||
}
|
||||
|
||||
class MyViewer(graph: Graph) : Viewer(graph, Viewer.ThreadingModel.GRAPH_IN_ANOTHER_THREAD) {
|
||||
class MyViewer(graph: Graph) : Viewer(graph, ThreadingModel.GRAPH_IN_ANOTHER_THREAD) {
|
||||
override fun enableAutoLayout(layoutAlgorithm: Layout) {
|
||||
super.enableAutoLayout(layoutAlgorithm)
|
||||
|
@ -1,9 +1,9 @@
|
||||
package core.visualiser
|
||||
package node.visualiser
|
||||
|
||||
import core.contracts.CommandData
|
||||
import core.contracts.ContractState
|
||||
import core.crypto.SecureHash
|
||||
import core.testutils.TransactionGroupDSL
|
||||
import core.testing.TransactionGroupDSL
|
||||
import org.graphstream.graph.Edge
|
||||
import org.graphstream.graph.Node
|
||||
import org.graphstream.graph.implementations.SingleGraph
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="core.visualiser.StateViewer">
|
||||
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="node.visualiser.StateViewer">
|
||||
<grid id="27dc6" binding="root" layout-manager="BorderLayout" hgap="15" vgap="15">
|
||||
<constraints>
|
||||
<xy x="20" y="20" width="500" height="400"/>
|
@ -1,4 +1,4 @@
|
||||
package core.visualiser;
|
||||
package node.visualiser;
|
||||
|
||||
import kotlin.*;
|
||||
|
Binary file not shown.
Reference in New Issue
Block a user