mirror of
https://github.com/corda/corda.git
synced 2025-06-17 06:38:21 +00:00
Requery removal (#1276)
* Removed Requery object relational mapping usage (and associated schemas including node-schemas module) * Fixed issues with NodeAttachmentService tests. Cannot use JPA custom converters with Primary Key fields. Hibernate entities require explicit call to flush() to persist to disk. * Removed redundant requery converters (equivalents not even required in Hibernate). * Removed remaining gradle requery dependency definitions. * Fixed broken tests. * Fixes for failing NodeVaultService tests: - Dynamic SQL updates (in soft locking code) - Explicit request by session to participate in transaction (causing "TransactionRequiredException" Executing an update/delete query) - Explicit flush() required to persist to disk * Updated changelog. Fixed compiler warning. * Fixed WHERE clause AND/OR condition. Enforced immediate data visibility through transaction commit. * Final fixes to address failing tests. * Deferred all hibernate session/txn management to DatabaseTransactionManager. * Fixed transaction boundaries in failing Cash tests. * Fixes to address failing tests (transaction boundaries, merge detached object, config clean-up). * Final adjustment to transaction boundaries in JUnit tests. * Refactored AttachmentSchemaV1 into NodeAttachmentService itself and referenced from NodeServicesV1. * Refactored HSQL UPDATE statements to use CriteriaUpdate API. * Updated all criteria API getters to reference attribute names by type. * Remove redundant VaultSchema entity name (required when previously using HSQL UPDATE syntax) * Fix compiler warnings. * Minor changes following rebase from master. * Fixed suppress warning type.
This commit is contained in:
@ -412,7 +412,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
||||
private fun makeServices(): MutableList<Any> {
|
||||
checkpointStorage = DBCheckpointStorage()
|
||||
_services = ServiceHubInternalImpl()
|
||||
attachments = NodeAttachmentService(configuration.dataSourceProperties, services.monitoringService.metrics, configuration.database)
|
||||
attachments = NodeAttachmentService(services.monitoringService.metrics)
|
||||
val legalIdentity = obtainIdentity("identity", configuration.myLegalName)
|
||||
network = makeMessagingService(legalIdentity)
|
||||
info = makeInfo(legalIdentity)
|
||||
@ -749,6 +749,9 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
||||
protected open fun generateKeyPair() = cryptoGenerateKeyPair()
|
||||
|
||||
private inner class ServiceHubInternalImpl : ServiceHubInternal, SingletonSerializeAsToken() {
|
||||
|
||||
private val hibernateConfig by lazy { HibernateConfiguration(schemaService, configuration.database ?: Properties(), { identityService }) }
|
||||
|
||||
override val rpcFlows = ArrayList<Class<out FlowLogic<*>>>()
|
||||
override val stateMachineRecordedTransactionMapping = DBTransactionMappingStorage()
|
||||
override val auditService = DummyAuditService()
|
||||
@ -756,9 +759,9 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
||||
override val validatedTransactions = makeTransactionStorage()
|
||||
override val transactionVerifierService by lazy { makeTransactionVerifierService() }
|
||||
override val networkMapCache by lazy { InMemoryNetworkMapCache(this) }
|
||||
override val vaultService by lazy { NodeVaultService(this, configuration.dataSourceProperties, configuration.database) }
|
||||
override val vaultService by lazy { NodeVaultService(this) }
|
||||
override val vaultQueryService by lazy {
|
||||
HibernateVaultQueryImpl(HibernateConfiguration(schemaService, configuration.database ?: Properties(), { identityService }), vaultService)
|
||||
HibernateVaultQueryImpl(hibernateConfig, vaultService)
|
||||
}
|
||||
// Place the long term identity key in the KMS. Eventually, this is likely going to be separated again because
|
||||
// the KMS is meant for derived temporary keys used in transactions, and we're not supposed to sign things with
|
||||
|
@ -7,6 +7,7 @@ import net.corda.core.schemas.converters.AbstractPartyToX500NameAsStringConverte
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.node.services.api.SchemaService
|
||||
import net.corda.node.utilities.DatabaseTransactionManager
|
||||
import net.corda.node.utilities.parserTransactionIsolationLevel
|
||||
import org.hibernate.SessionFactory
|
||||
import org.hibernate.boot.MetadataSources
|
||||
import org.hibernate.boot.model.naming.Identifier
|
||||
@ -28,6 +29,8 @@ class HibernateConfiguration(val schemaService: SchemaService, val databasePrope
|
||||
// TODO: make this a guava cache or similar to limit ability for this to grow forever.
|
||||
val sessionFactories = ConcurrentHashMap<MappedSchema, SessionFactory>()
|
||||
|
||||
private val transactionIsolationLevel = parserTransactionIsolationLevel(databaseProperties.getProperty("transactionIsolationLevel") ?:"")
|
||||
|
||||
init {
|
||||
schemaService.schemaOptions.map { it.key }.forEach { mappedSchema ->
|
||||
sessionFactories.computeIfAbsent(mappedSchema, { makeSessionFactoryForSchema(mappedSchema) })
|
||||
@ -60,6 +63,7 @@ class HibernateConfiguration(val schemaService: SchemaService, val databasePrope
|
||||
val config = Configuration(metadataSources).setProperty("hibernate.connection.provider_class", HibernateConfiguration.NodeDatabaseConnectionProvider::class.java.name)
|
||||
.setProperty("hibernate.hbm2ddl.auto", if (databaseProperties.getProperty("initDatabase","true") == "true") "update" else "validate")
|
||||
.setProperty("hibernate.format_sql", "true")
|
||||
.setProperty("hibernate.connection.isolation", transactionIsolationLevel.toString())
|
||||
|
||||
schemas.forEach { schema ->
|
||||
// TODO: require mechanism to set schemaOptions (databaseSchema, tablePrefix) which are not global to session
|
||||
|
@ -1,146 +0,0 @@
|
||||
package net.corda.node.services.database
|
||||
|
||||
import io.requery.EntityCache
|
||||
import io.requery.TransactionIsolation
|
||||
import io.requery.TransactionListener
|
||||
import io.requery.cache.WeakEntityCache
|
||||
import io.requery.meta.EntityModel
|
||||
import io.requery.sql.*
|
||||
import io.requery.sql.platform.H2
|
||||
import io.requery.util.function.Function
|
||||
import io.requery.util.function.Supplier
|
||||
import net.corda.core.schemas.requery.converters.InstantConverter
|
||||
import net.corda.core.schemas.requery.converters.SecureHashConverter
|
||||
import net.corda.core.schemas.requery.converters.StateRefConverter
|
||||
import net.corda.core.schemas.requery.converters.VaultStateStatusConverter
|
||||
import net.corda.node.utilities.DatabaseTransactionManager
|
||||
import java.sql.Connection
|
||||
import java.util.*
|
||||
import java.util.concurrent.Executor
|
||||
import javax.sql.DataSource
|
||||
|
||||
/**
|
||||
* Requery KotlinConfiguration wrapper class to enable us to pass in an existing database connection and
|
||||
* associated transaction context.
|
||||
*/
|
||||
class KotlinConfigurationTransactionWrapper(private val model: EntityModel,
|
||||
dataSource: DataSource,
|
||||
private val mapping: Mapping? = null,
|
||||
private val platform: Platform? = null,
|
||||
private val cache: EntityCache = WeakEntityCache(),
|
||||
private val useDefaultLogging: Boolean = false,
|
||||
private val statementCacheSize: Int = 0,
|
||||
private val batchUpdateSize: Int = 64,
|
||||
private val quoteTableNames: Boolean = false,
|
||||
private val quoteColumnNames: Boolean = false,
|
||||
private val tableTransformer: Function<String, String>? = null,
|
||||
private val columnTransformer: Function<String, String>? = null,
|
||||
private val transactionMode: TransactionMode = TransactionMode.NONE,
|
||||
private val transactionIsolation: TransactionIsolation? = null,
|
||||
private val statementListeners: Set<StatementListener> = LinkedHashSet(),
|
||||
private val entityStateListeners: Set<EntityStateListener<Any>> = LinkedHashSet(),
|
||||
private val transactionListeners: Set<Supplier<TransactionListener>> = LinkedHashSet(),
|
||||
private val writeExecutor: Executor? = null) : Configuration {
|
||||
|
||||
private val connectionProvider = CordaDataSourceConnectionProvider(dataSource)
|
||||
|
||||
override fun getBatchUpdateSize(): Int {
|
||||
return batchUpdateSize
|
||||
}
|
||||
|
||||
override fun getConnectionProvider(): ConnectionProvider? {
|
||||
return connectionProvider
|
||||
}
|
||||
|
||||
override fun getCache(): EntityCache? {
|
||||
return cache
|
||||
}
|
||||
|
||||
override fun getEntityStateListeners(): Set<EntityStateListener<Any>> {
|
||||
return entityStateListeners
|
||||
}
|
||||
|
||||
override fun getMapping(): Mapping? {
|
||||
// TODO: database platform provider to become configurable and parameterised into this configuration
|
||||
val customMapping = GenericMapping(H2())
|
||||
|
||||
// register our custom converters
|
||||
val instantConverter = InstantConverter()
|
||||
customMapping.addConverter(instantConverter, instantConverter.mappedType)
|
||||
val vaultStateStatusConverter = VaultStateStatusConverter()
|
||||
customMapping.addConverter(vaultStateStatusConverter, vaultStateStatusConverter.mappedType)
|
||||
customMapping.addConverter(StateRefConverter(), StateRefConverter::getMappedType.javaClass)
|
||||
customMapping.addConverter(SecureHashConverter(), SecureHashConverter::getMappedType.javaClass)
|
||||
|
||||
return customMapping
|
||||
}
|
||||
|
||||
override fun getModel(): EntityModel {
|
||||
return model
|
||||
}
|
||||
|
||||
override fun getPlatform(): Platform? {
|
||||
return platform
|
||||
}
|
||||
|
||||
override fun getQuoteTableNames(): Boolean {
|
||||
return quoteTableNames
|
||||
}
|
||||
|
||||
override fun getQuoteColumnNames(): Boolean {
|
||||
return quoteColumnNames
|
||||
}
|
||||
|
||||
override fun getTableTransformer(): Function<String, String>? {
|
||||
return tableTransformer
|
||||
}
|
||||
|
||||
override fun getColumnTransformer(): Function<String, String>? {
|
||||
return columnTransformer
|
||||
}
|
||||
|
||||
override fun getStatementCacheSize(): Int {
|
||||
return statementCacheSize
|
||||
}
|
||||
|
||||
override fun getStatementListeners(): Set<StatementListener>? {
|
||||
return statementListeners
|
||||
}
|
||||
|
||||
override fun getTransactionMode(): TransactionMode? {
|
||||
return transactionMode
|
||||
}
|
||||
|
||||
override fun getTransactionIsolation(): TransactionIsolation? {
|
||||
return transactionIsolation
|
||||
}
|
||||
|
||||
override fun getTransactionListenerFactories(): Set<Supplier<TransactionListener>>? {
|
||||
return transactionListeners
|
||||
}
|
||||
|
||||
override fun getUseDefaultLogging(): Boolean {
|
||||
return useDefaultLogging
|
||||
}
|
||||
|
||||
override fun getWriteExecutor(): Executor? {
|
||||
return writeExecutor
|
||||
}
|
||||
|
||||
class CordaDataSourceConnectionProvider(val dataSource: DataSource) : ConnectionProvider {
|
||||
override fun getConnection(): Connection = CordaConnection(DatabaseTransactionManager.current().connection)
|
||||
}
|
||||
|
||||
class CordaConnection(val connection: Connection) : Connection by connection {
|
||||
override fun close() {
|
||||
// TODO: address requery auto-closing the connection in SchemaModifier upon table creation
|
||||
// https://github.com/requery/requery/issues/424
|
||||
}
|
||||
|
||||
override fun setAutoCommit(autoCommit: Boolean) {
|
||||
// TODO: address requery bug in ConnectionTransaction commit()
|
||||
// https://github.com/requery/requery/issues/423
|
||||
connection.autoCommit = false
|
||||
}
|
||||
}
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
package net.corda.node.services.database
|
||||
|
||||
import com.zaxxer.hikari.HikariConfig
|
||||
import com.zaxxer.hikari.HikariDataSource
|
||||
import io.requery.Persistable
|
||||
import io.requery.TransactionIsolation
|
||||
import io.requery.meta.EntityModel
|
||||
import io.requery.sql.KotlinEntityDataStore
|
||||
import io.requery.sql.SchemaModifier
|
||||
import io.requery.sql.TableCreationMode
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.node.utilities.DatabaseTransactionManager
|
||||
import java.sql.Connection
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class RequeryConfiguration(val properties: Properties, val useDefaultLogging: Boolean = false, val databaseProperties: Properties) {
|
||||
|
||||
companion object {
|
||||
val logger = loggerFor<RequeryConfiguration>()
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// 1. schemaService schemaOptions needs to be applied: eg. default schema, table prefix
|
||||
// 2. set other generic database configuration options: show_sql, format_sql
|
||||
// 3. Configure Requery Database platform specific features (see http://requery.github.io/javadoc/io/requery/sql/Platform.html)
|
||||
// 4. Configure Cache Manager and Cache Provider and set in Requery Configuration (see http://requery.github.io/javadoc/io/requery/EntityCache.html)
|
||||
// 5. Consider database schema deployment/upgrade strategies to replace dynamic table creation.
|
||||
|
||||
// Note: Annotations are pre-processed using (kapt) so no need to register dynamically
|
||||
val config = HikariConfig(properties)
|
||||
val dataSource = HikariDataSource(config)
|
||||
|
||||
// TODO: make this a guava cache or similar to limit ability for this to grow forever.
|
||||
private val sessionFactories = ConcurrentHashMap<EntityModel, KotlinEntityDataStore<Persistable>>()
|
||||
|
||||
fun sessionForModel(model: EntityModel): KotlinEntityDataStore<Persistable> {
|
||||
return sessionFactories.computeIfAbsent(model, { makeSessionFactoryForModel(it) })
|
||||
}
|
||||
|
||||
fun makeSessionFactoryForModel(model: EntityModel): KotlinEntityDataStore<Persistable> {
|
||||
val configuration = KotlinConfigurationTransactionWrapper(model, dataSource, useDefaultLogging = this.useDefaultLogging)
|
||||
val tables = SchemaModifier(configuration)
|
||||
if (databaseProperties.getProperty("initDatabase","true") == "true" ) {
|
||||
val mode = TableCreationMode.CREATE_NOT_EXISTS
|
||||
tables.createTables(mode)
|
||||
}
|
||||
return KotlinEntityDataStore(configuration)
|
||||
}
|
||||
|
||||
// TODO: remove once Requery supports QUERY WITH COMPOSITE_KEY IN
|
||||
fun jdbcSession(): Connection = DatabaseTransactionManager.current().connection
|
||||
}
|
||||
|
||||
fun parserTransactionIsolationLevel(property: String?) : TransactionIsolation =
|
||||
when (property) {
|
||||
"none" -> TransactionIsolation.NONE
|
||||
"readUncommitted" -> TransactionIsolation.READ_UNCOMMITTED
|
||||
"readCommitted" -> TransactionIsolation.READ_COMMITTED
|
||||
"repeatableRead" -> TransactionIsolation.REPEATABLE_READ
|
||||
"serializable" -> TransactionIsolation.SERIALIZABLE
|
||||
else -> {
|
||||
TransactionIsolation.REPEATABLE_READ
|
||||
}
|
||||
}
|
@ -12,41 +12,50 @@ import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.node.services.AttachmentStorage
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.node.services.database.RequeryConfiguration
|
||||
import net.corda.node.services.persistence.schemas.requery.AttachmentEntity
|
||||
import net.corda.node.services.persistence.schemas.requery.Models
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.FilterInputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import net.corda.node.utilities.DatabaseTransactionManager
|
||||
import net.corda.node.utilities.NODE_DATABASE_PREFIX
|
||||
import java.io.*
|
||||
import java.nio.file.FileAlreadyExistsException
|
||||
import java.nio.file.Paths
|
||||
import java.util.*
|
||||
import java.util.jar.JarInputStream
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
import javax.persistence.*
|
||||
|
||||
/**
|
||||
* Stores attachments in H2 database.
|
||||
* Stores attachments using Hibernate to database.
|
||||
*/
|
||||
@ThreadSafe
|
||||
class NodeAttachmentService(dataSourceProperties: Properties, metrics: MetricRegistry, databaseProperties: Properties?)
|
||||
: AttachmentStorage, SingletonSerializeAsToken() {
|
||||
class NodeAttachmentService(metrics: MetricRegistry) : AttachmentStorage, SingletonSerializeAsToken() {
|
||||
|
||||
@Entity
|
||||
@Table(name = "${NODE_DATABASE_PREFIX}attachments",
|
||||
indexes = arrayOf(Index(name = "att_id_idx", columnList = "att_id")))
|
||||
class DBAttachment(
|
||||
@Id
|
||||
@Column(name = "att_id", length = 65535)
|
||||
var attId: String,
|
||||
|
||||
@Column(name = "content")
|
||||
@Lob
|
||||
var content: ByteArray
|
||||
) : Serializable
|
||||
|
||||
companion object {
|
||||
private val log = loggerFor<NodeAttachmentService>()
|
||||
}
|
||||
|
||||
val configuration = RequeryConfiguration(dataSourceProperties, databaseProperties = databaseProperties ?: Properties())
|
||||
val session = configuration.sessionForModel(Models.PERSISTENCE)
|
||||
|
||||
@VisibleForTesting
|
||||
var checkAttachmentsOnLoad = true
|
||||
|
||||
private val attachmentCount = metrics.counter("Attachments")
|
||||
|
||||
init {
|
||||
session.withTransaction {
|
||||
attachmentCount.inc(session.count(AttachmentEntity::class).get().value().toLong())
|
||||
}
|
||||
val session = DatabaseTransactionManager.current().session
|
||||
val criteriaBuilder = session.criteriaBuilder
|
||||
val criteriaQuery = criteriaBuilder.createQuery(Long::class.java)
|
||||
criteriaQuery.select(criteriaBuilder.count(criteriaQuery.from(NodeAttachmentService.DBAttachment::class.java)))
|
||||
val count = session.createQuery(criteriaQuery).singleResult
|
||||
attachmentCount.inc(count)
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
@ -128,16 +137,13 @@ class NodeAttachmentService(dataSourceProperties: Properties, metrics: MetricReg
|
||||
|
||||
}
|
||||
|
||||
override fun openAttachment(id: SecureHash): Attachment? = session.withTransaction {
|
||||
try {
|
||||
session.select(AttachmentEntity::class)
|
||||
.where(AttachmentEntity.ATT_ID.eq(id))
|
||||
.get()
|
||||
.single()
|
||||
} catch (e: NoSuchElementException) {
|
||||
null
|
||||
override fun openAttachment(id: SecureHash): Attachment? {
|
||||
val attachment = DatabaseTransactionManager.current().session.get(NodeAttachmentService.DBAttachment::class.java, id.toString())
|
||||
attachment?.let {
|
||||
return AttachmentImpl(id, { attachment.content }, checkAttachmentsOnLoad)
|
||||
}
|
||||
}?.run { AttachmentImpl(id, { content }, checkAttachmentsOnLoad) }
|
||||
return null
|
||||
}
|
||||
|
||||
// TODO: PLT-147: The attachment should be randomised to prevent brute force guessing and thus privacy leaks.
|
||||
override fun importAttachment(jar: InputStream): SecureHash {
|
||||
@ -153,25 +159,21 @@ class NodeAttachmentService(dataSourceProperties: Properties, metrics: MetricReg
|
||||
checkIsAValidJAR(ByteArrayInputStream(bytes))
|
||||
val id = SecureHash.SHA256(hs.hash().asBytes())
|
||||
|
||||
val count = session.withTransaction {
|
||||
session.count(AttachmentEntity::class)
|
||||
.where(AttachmentEntity.ATT_ID.eq(id))
|
||||
.get().value()
|
||||
}
|
||||
|
||||
val session = DatabaseTransactionManager.current().session
|
||||
val criteriaBuilder = session.criteriaBuilder
|
||||
val criteriaQuery = criteriaBuilder.createQuery(Long::class.java)
|
||||
val attachments = criteriaQuery.from(NodeAttachmentService.DBAttachment::class.java)
|
||||
criteriaQuery.select(criteriaBuilder.count(criteriaQuery.from(NodeAttachmentService.DBAttachment::class.java)))
|
||||
criteriaQuery.where(criteriaBuilder.equal(attachments.get<String>(DBAttachment::attId.name), id.toString()))
|
||||
val count = session.createQuery(criteriaQuery).singleResult
|
||||
if (count > 0) {
|
||||
throw FileAlreadyExistsException(id.toString())
|
||||
}
|
||||
|
||||
session.withTransaction {
|
||||
val attachment = AttachmentEntity()
|
||||
attachment.attId = id
|
||||
attachment.content = bytes
|
||||
session.insert(attachment)
|
||||
}
|
||||
val attachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = bytes)
|
||||
session.save(attachment)
|
||||
|
||||
attachmentCount.inc()
|
||||
|
||||
log.info("Stored new attachment $id")
|
||||
|
||||
return id
|
||||
|
@ -14,6 +14,7 @@ import net.corda.node.services.keys.PersistentKeyManagementService
|
||||
import net.corda.node.services.persistence.DBCheckpointStorage
|
||||
import net.corda.node.services.persistence.DBTransactionMappingStorage
|
||||
import net.corda.node.services.persistence.DBTransactionStorage
|
||||
import net.corda.node.services.persistence.NodeAttachmentService
|
||||
import net.corda.node.services.transactions.PersistentUniquenessProvider
|
||||
import net.corda.node.services.vault.VaultSchemaV1
|
||||
|
||||
@ -36,7 +37,9 @@ class NodeSchemaService(customSchemas: Set<MappedSchema> = emptySet()) : SchemaS
|
||||
DBTransactionMappingStorage.DBTransactionMapping::class.java,
|
||||
PersistentKeyManagementService.PersistentKey::class.java,
|
||||
PersistentUniquenessProvider.PersistentUniqueness::class.java,
|
||||
NodeSchedulerService.PersistentScheduledState::class.java
|
||||
PersistentUniquenessProvider.PersistentUniqueness::class.java,
|
||||
NodeSchedulerService.PersistentScheduledState::class.java,
|
||||
NodeAttachmentService.DBAttachment::class.java
|
||||
))
|
||||
|
||||
// Required schemas are those used by internal Corda services
|
||||
@ -46,7 +49,6 @@ class NodeSchemaService(customSchemas: Set<MappedSchema> = emptySet()) : SchemaS
|
||||
Pair(VaultSchemaV1, SchemaService.SchemaOptions()),
|
||||
Pair(NodeServicesV1, SchemaService.SchemaOptions()))
|
||||
|
||||
|
||||
override val schemaOptions: Map<MappedSchema, SchemaService.SchemaOptions> = requiredSchemas.plus(customSchemas.map {
|
||||
mappedSchema -> Pair(mappedSchema, SchemaService.SchemaOptions())
|
||||
})
|
||||
|
@ -191,7 +191,7 @@ object BFTSMaRt {
|
||||
}
|
||||
|
||||
override fun getStateManager() = stateManagerOverride
|
||||
// TODO: Use Requery with proper DB schema instead of JDBCHashMap.
|
||||
// TODO: Use proper DB schema instead of JDBCHashMap.
|
||||
// Must be initialised before ServiceReplica is started
|
||||
private val commitLog = services.database.transaction { JDBCHashMap<StateRef, UniquenessProvider.ConsumingTx>(tableName) }
|
||||
private val replica = run {
|
||||
|
@ -2,15 +2,11 @@ package net.corda.node.services.vault
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import co.paralleluniverse.strands.Strand
|
||||
import net.corda.core.internal.VisibleForTesting
|
||||
import io.requery.PersistenceException
|
||||
import io.requery.kotlin.eq
|
||||
import io.requery.query.RowExpression
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.containsAny
|
||||
import net.corda.core.crypto.toBase58String
|
||||
import net.corda.core.internal.ThreadBox
|
||||
import net.corda.core.internal.VisibleForTesting
|
||||
import net.corda.core.internal.tee
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.services.StatesNotAvailableException
|
||||
@ -21,6 +17,7 @@ import net.corda.core.node.services.vault.QueryCriteria
|
||||
import net.corda.core.node.services.vault.Sort
|
||||
import net.corda.core.node.services.vault.SortAttribute
|
||||
import net.corda.core.schemas.PersistentState
|
||||
import net.corda.core.schemas.PersistentStateRef
|
||||
import net.corda.core.serialization.SerializationDefaults.STORAGE_CONTEXT
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.serialization.deserialize
|
||||
@ -28,22 +25,15 @@ import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.CoreTransaction
|
||||
import net.corda.core.transactions.NotaryChangeWireTransaction
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.core.utilities.NonEmptySet
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.core.utilities.toNonEmptySet
|
||||
import net.corda.core.utilities.trace
|
||||
import net.corda.node.services.database.RequeryConfiguration
|
||||
import net.corda.node.services.database.parserTransactionIsolationLevel
|
||||
import net.corda.core.utilities.*
|
||||
import net.corda.node.services.statemachine.FlowStateMachineImpl
|
||||
import net.corda.node.services.vault.schemas.requery.Models
|
||||
import net.corda.node.services.vault.schemas.requery.VaultSchema
|
||||
import net.corda.node.services.vault.schemas.requery.VaultStatesEntity
|
||||
import net.corda.node.services.vault.schemas.requery.VaultTxnNoteEntity
|
||||
import net.corda.node.utilities.DatabaseTransactionManager
|
||||
import net.corda.node.utilities.bufferUntilDatabaseCommit
|
||||
import net.corda.node.utilities.wrapWithDatabaseTransaction
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import javax.persistence.criteria.Predicate
|
||||
|
||||
@ -54,23 +44,15 @@ import javax.persistence.criteria.Predicate
|
||||
* This class needs database transactions to be in-flight during method calls and init, and will throw exceptions if
|
||||
* this is not the case.
|
||||
*
|
||||
* TODO: move query / filter criteria into the database query.
|
||||
* TODO: keep an audit trail with time stamps of previously unconsumed states "as of" a particular point in time.
|
||||
* TODO: have transaction storage do some caching.
|
||||
*/
|
||||
class NodeVaultService(private val services: ServiceHub, dataSourceProperties: Properties, databaseProperties: Properties?) : SingletonSerializeAsToken(), VaultService {
|
||||
class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsToken(), VaultService {
|
||||
|
||||
private companion object {
|
||||
val log = loggerFor<NodeVaultService>()
|
||||
|
||||
// Define composite primary key used in Requery Expression
|
||||
val stateRefCompositeColumn: RowExpression = RowExpression.of(listOf(VaultStatesEntity.TX_ID, VaultStatesEntity.INDEX))
|
||||
}
|
||||
|
||||
val configuration = RequeryConfiguration(dataSourceProperties, databaseProperties = databaseProperties ?: Properties())
|
||||
val session = configuration.sessionForModel(Models.VAULT)
|
||||
private val transactionIsolationLevel = parserTransactionIsolationLevel(databaseProperties?.getProperty("transactionIsolationLevel") ?:"")
|
||||
|
||||
private class InnerState {
|
||||
val _updatesPublisher = PublishSubject.create<Vault.Update<ContractState>>()!!
|
||||
val _rawUpdatesPublisher = PublishSubject.create<Vault.Update<ContractState>>()!!
|
||||
@ -89,35 +71,29 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P
|
||||
val consumedStateRefs = update.consumed.map { it.ref }
|
||||
log.trace { "Removing $consumedStateRefs consumed contract states and adding $producedStateRefs produced contract states to the database." }
|
||||
|
||||
session.withTransaction(transactionIsolationLevel) {
|
||||
producedStateRefsMap.forEach { it ->
|
||||
val state = VaultStatesEntity().apply {
|
||||
txId = it.key.txhash.toString()
|
||||
index = it.key.index
|
||||
stateStatus = Vault.StateStatus.UNCONSUMED
|
||||
contractStateClassName = it.value.state.data.javaClass.name
|
||||
contractState = it.value.state.serialize(context = STORAGE_CONTEXT).bytes
|
||||
notaryName = it.value.state.notary.name.toString()
|
||||
recordedTime = services.clock.instant()
|
||||
}
|
||||
insert(state)
|
||||
}
|
||||
// TODO: awaiting support of UPDATE WHERE <Composite key> IN in Requery DSL
|
||||
consumedStateRefs.forEach { stateRef ->
|
||||
val queryKey = io.requery.proxy.CompositeKey(mapOf(VaultStatesEntity.TX_ID to stateRef.txhash.toString(),
|
||||
VaultStatesEntity.INDEX to stateRef.index))
|
||||
val state = findByKey(VaultStatesEntity::class, queryKey)
|
||||
state?.run {
|
||||
stateStatus = Vault.StateStatus.CONSUMED
|
||||
consumedTime = services.clock.instant()
|
||||
// remove lock (if held)
|
||||
if (lockId != null) {
|
||||
lockId = null
|
||||
lockUpdateTime = services.clock.instant()
|
||||
log.trace("Releasing soft lock on consumed state: $stateRef")
|
||||
}
|
||||
update(state)
|
||||
val session = DatabaseTransactionManager.current().session
|
||||
producedStateRefsMap.forEach { stateAndRef ->
|
||||
val state = VaultSchemaV1.VaultStates(
|
||||
notary = stateAndRef.value.state.notary,
|
||||
contractStateClassName = stateAndRef.value.state.data.javaClass.name,
|
||||
contractState = stateAndRef.value.state.serialize(context = STORAGE_CONTEXT).bytes,
|
||||
stateStatus = Vault.StateStatus.UNCONSUMED,
|
||||
recordedTime = services.clock.instant())
|
||||
state.stateRef = PersistentStateRef(stateAndRef.key)
|
||||
session.save(state)
|
||||
}
|
||||
consumedStateRefs.forEach { stateRef ->
|
||||
val state = session.get<VaultSchemaV1.VaultStates>(VaultSchemaV1.VaultStates::class.java, PersistentStateRef(stateRef))
|
||||
state?.run {
|
||||
stateStatus = Vault.StateStatus.CONSUMED
|
||||
consumedTime = services.clock.instant()
|
||||
// remove lock (if held)
|
||||
if (lockId != null) {
|
||||
lockId = null
|
||||
lockUpdateTime = services.clock.instant()
|
||||
log.trace("Releasing soft lock on consumed state: $stateRef")
|
||||
}
|
||||
session.save(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -220,19 +196,25 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P
|
||||
processAndNotify(netDelta)
|
||||
}
|
||||
|
||||
// TODO: replace this method in favour of a VaultQuery query
|
||||
private fun loadStates(refs: Collection<StateRef>): HashSet<StateAndRef<ContractState>> {
|
||||
val states = HashSet<StateAndRef<ContractState>>()
|
||||
if (refs.isNotEmpty()) {
|
||||
session.withTransaction(transactionIsolationLevel) {
|
||||
val result = select(VaultStatesEntity::class).
|
||||
where(stateRefCompositeColumn.`in`(stateRefArgs(refs))).
|
||||
and(VaultSchema.VaultStates::stateStatus eq Vault.StateStatus.UNCONSUMED)
|
||||
result.get().forEach {
|
||||
val txHash = SecureHash.parse(it.txId)
|
||||
val index = it.index
|
||||
val state = it.contractState.deserialize<TransactionState<ContractState>>(context = STORAGE_CONTEXT)
|
||||
states.add(StateAndRef(state, StateRef(txHash, index)))
|
||||
}
|
||||
val session = DatabaseTransactionManager.current().session
|
||||
val criteriaBuilder = session.criteriaBuilder
|
||||
val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultStates::class.java)
|
||||
val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java)
|
||||
val statusPredicate = criteriaBuilder.equal(vaultStates.get<Vault.StateStatus>(VaultSchemaV1.VaultStates::stateStatus.name), Vault.StateStatus.UNCONSUMED)
|
||||
val persistentStateRefs = refs.map { PersistentStateRef(it.txhash.bytes.toHexString(), it.index) }
|
||||
val compositeKey = vaultStates.get<PersistentStateRef>(VaultSchemaV1.VaultStates::stateRef.name)
|
||||
val stateRefsPredicate = criteriaBuilder.and(compositeKey.`in`(persistentStateRefs))
|
||||
criteriaQuery.where(statusPredicate, stateRefsPredicate)
|
||||
val results = session.createQuery(criteriaQuery).resultList
|
||||
results.asSequence().forEach {
|
||||
val txHash = SecureHash.parse(it.stateRef?.txId!!)
|
||||
val index = it.stateRef?.index!!
|
||||
val state = it.contractState.deserialize<TransactionState<ContractState>>(context = STORAGE_CONTEXT)
|
||||
states.add(StateAndRef(state, StateRef(txHash, index)))
|
||||
}
|
||||
}
|
||||
return states
|
||||
@ -251,85 +233,105 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P
|
||||
}
|
||||
|
||||
override fun addNoteToTransaction(txnId: SecureHash, noteText: String) {
|
||||
session.withTransaction(transactionIsolationLevel) {
|
||||
val txnNoteEntity = VaultTxnNoteEntity()
|
||||
txnNoteEntity.txId = txnId.toString()
|
||||
txnNoteEntity.note = noteText
|
||||
insert(txnNoteEntity)
|
||||
}
|
||||
val txnNoteEntity = VaultSchemaV1.VaultTxnNote(txnId.toString(), noteText)
|
||||
DatabaseTransactionManager.current().session.save(txnNoteEntity)
|
||||
}
|
||||
|
||||
override fun getTransactionNotes(txnId: SecureHash): Iterable<String> {
|
||||
return session.withTransaction(transactionIsolationLevel) {
|
||||
(select(VaultSchema.VaultTxnNote::class) where (VaultSchema.VaultTxnNote::txId eq txnId.toString())).get().asIterable().map { it.note }
|
||||
}
|
||||
val session = DatabaseTransactionManager.current().session
|
||||
val criteriaBuilder = session.criteriaBuilder
|
||||
val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultTxnNote::class.java)
|
||||
val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultTxnNote::class.java)
|
||||
val txIdPredicate = criteriaBuilder.equal(vaultStates.get<Vault.StateStatus>(VaultSchemaV1.VaultTxnNote::txId.name), txnId.toString())
|
||||
criteriaQuery.where(txIdPredicate)
|
||||
val results = session.createQuery(criteriaQuery).resultList
|
||||
return results.asIterable().map { it.note }
|
||||
}
|
||||
|
||||
@Throws(StatesNotAvailableException::class)
|
||||
override fun softLockReserve(lockId: UUID, stateRefs: NonEmptySet<StateRef>) {
|
||||
val softLockTimestamp = services.clock.instant()
|
||||
val stateRefArgs = stateRefArgs(stateRefs)
|
||||
try {
|
||||
session.withTransaction(transactionIsolationLevel) {
|
||||
val updatedRows = update(VaultStatesEntity::class)
|
||||
.set(VaultStatesEntity.LOCK_ID, lockId.toString())
|
||||
.set(VaultStatesEntity.LOCK_UPDATE_TIME, softLockTimestamp)
|
||||
.where(VaultStatesEntity.STATE_STATUS eq Vault.StateStatus.UNCONSUMED)
|
||||
.and((VaultStatesEntity.LOCK_ID eq lockId.toString()) or (VaultStatesEntity.LOCK_ID.isNull()))
|
||||
.and(stateRefCompositeColumn.`in`(stateRefArgs)).get().value()
|
||||
if (updatedRows > 0 && updatedRows == stateRefs.size) {
|
||||
log.trace("Reserving soft lock states for $lockId: $stateRefs")
|
||||
FlowStateMachineImpl.currentStateMachine()?.hasSoftLockedStates = true
|
||||
} else {
|
||||
// revert partial soft locks
|
||||
val revertUpdatedRows = update(VaultStatesEntity::class)
|
||||
.set(VaultStatesEntity.LOCK_ID, null)
|
||||
.where(VaultStatesEntity.LOCK_UPDATE_TIME eq softLockTimestamp)
|
||||
.and(VaultStatesEntity.LOCK_ID eq lockId.toString())
|
||||
.and(stateRefCompositeColumn.`in`(stateRefArgs)).get().value()
|
||||
if (revertUpdatedRows > 0) {
|
||||
log.trace("Reverting $revertUpdatedRows partially soft locked states for $lockId")
|
||||
}
|
||||
throw StatesNotAvailableException("Attempted to reserve $stateRefs for $lockId but only $updatedRows rows available")
|
||||
val session = DatabaseTransactionManager.current().session
|
||||
val criteriaBuilder = session.criteriaBuilder
|
||||
val criteriaUpdate = criteriaBuilder.createCriteriaUpdate(VaultSchemaV1.VaultStates::class.java)
|
||||
val vaultStates = criteriaUpdate.from(VaultSchemaV1.VaultStates::class.java)
|
||||
val stateStatusPredication = criteriaBuilder.equal(vaultStates.get<Vault.StateStatus>(VaultSchemaV1.VaultStates::stateStatus.name), Vault.StateStatus.UNCONSUMED)
|
||||
val lockIdPredicate = criteriaBuilder.or(vaultStates.get<String>(VaultSchemaV1.VaultStates::lockId.name).isNull,
|
||||
criteriaBuilder.equal(vaultStates.get<String>(VaultSchemaV1.VaultStates::lockId.name), lockId.toString()))
|
||||
val persistentStateRefs = stateRefs.map { PersistentStateRef(it.txhash.bytes.toHexString(), it.index) }
|
||||
val compositeKey = vaultStates.get<PersistentStateRef>(VaultSchemaV1.VaultStates::stateRef.name)
|
||||
val stateRefsPredicate = criteriaBuilder.and(compositeKey.`in`(persistentStateRefs))
|
||||
criteriaUpdate.set(vaultStates.get<String>(VaultSchemaV1.VaultStates::lockId.name), lockId.toString())
|
||||
criteriaUpdate.set(vaultStates.get<Instant>(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp)
|
||||
criteriaUpdate.where(stateStatusPredication, lockIdPredicate, stateRefsPredicate)
|
||||
val updatedRows = session.createQuery(criteriaUpdate).executeUpdate()
|
||||
if (updatedRows > 0 && updatedRows == stateRefs.size) {
|
||||
log.trace("Reserving soft lock states for $lockId: $stateRefs")
|
||||
FlowStateMachineImpl.currentStateMachine()?.hasSoftLockedStates = true
|
||||
} else {
|
||||
// revert partial soft locks
|
||||
val criteriaRevertUpdate = criteriaBuilder.createCriteriaUpdate(VaultSchemaV1.VaultStates::class.java)
|
||||
val vaultStatesRevert = criteriaRevertUpdate.from(VaultSchemaV1.VaultStates::class.java)
|
||||
val lockIdPredicateRevert = criteriaBuilder.equal(vaultStatesRevert.get<String>(VaultSchemaV1.VaultStates::lockId.name), lockId.toString())
|
||||
val lockUpdateTime = criteriaBuilder.equal(vaultStatesRevert.get<Instant>(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp)
|
||||
val persistentStateRefsRevert = stateRefs.map { PersistentStateRef(it.txhash.bytes.toHexString(), it.index) }
|
||||
val compositeKeyRevert = vaultStatesRevert.get<PersistentStateRef>(VaultSchemaV1.VaultStates::stateRef.name)
|
||||
val stateRefsPredicateRevert = criteriaBuilder.and(compositeKeyRevert.`in`(persistentStateRefsRevert))
|
||||
criteriaRevertUpdate.set(vaultStatesRevert.get<String>(VaultSchemaV1.VaultStates::lockId.name), criteriaBuilder.nullLiteral(String::class.java))
|
||||
criteriaRevertUpdate.where(lockUpdateTime, lockIdPredicateRevert, stateRefsPredicateRevert)
|
||||
val revertUpdatedRows = session.createQuery(criteriaRevertUpdate).executeUpdate()
|
||||
if (revertUpdatedRows > 0) {
|
||||
log.trace("Reverting $revertUpdatedRows partially soft locked states for $lockId")
|
||||
}
|
||||
throw StatesNotAvailableException("Attempted to reserve $stateRefs for $lockId but only $updatedRows rows available")
|
||||
}
|
||||
} catch (e: PersistenceException) {
|
||||
} catch (e: Exception) {
|
||||
log.error("""soft lock update error attempting to reserve states for $lockId and $stateRefs")
|
||||
$e.
|
||||
""")
|
||||
if (e.cause is StatesNotAvailableException) throw (e.cause as StatesNotAvailableException)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun softLockRelease(lockId: UUID, stateRefs: NonEmptySet<StateRef>?) {
|
||||
val softLockTimestamp = services.clock.instant()
|
||||
val session = DatabaseTransactionManager.current().session
|
||||
val criteriaBuilder = session.criteriaBuilder
|
||||
if (stateRefs == null) {
|
||||
session.withTransaction(transactionIsolationLevel) {
|
||||
val update = update(VaultStatesEntity::class)
|
||||
.set(VaultStatesEntity.LOCK_ID, null)
|
||||
.set(VaultStatesEntity.LOCK_UPDATE_TIME, services.clock.instant())
|
||||
.where(VaultStatesEntity.STATE_STATUS eq Vault.StateStatus.UNCONSUMED)
|
||||
.and(VaultStatesEntity.LOCK_ID eq lockId.toString()).get()
|
||||
if (update.value() > 0) {
|
||||
log.trace("Releasing ${update.value()} soft locked states for $lockId")
|
||||
}
|
||||
val criteriaUpdate = criteriaBuilder.createCriteriaUpdate(VaultSchemaV1.VaultStates::class.java)
|
||||
val vaultStates = criteriaUpdate.from(VaultSchemaV1.VaultStates::class.java)
|
||||
val stateStatusPredication = criteriaBuilder.equal(vaultStates.get<Vault.StateStatus>(VaultSchemaV1.VaultStates::stateStatus.name), Vault.StateStatus.UNCONSUMED)
|
||||
val lockIdPredicate = criteriaBuilder.equal(vaultStates.get<String>(VaultSchemaV1.VaultStates::lockId.name), lockId.toString())
|
||||
criteriaUpdate.set<String>(vaultStates.get<String>(VaultSchemaV1.VaultStates::lockId.name), criteriaBuilder.nullLiteral(String::class.java))
|
||||
criteriaUpdate.set(vaultStates.get<Instant>(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp)
|
||||
criteriaUpdate.where(stateStatusPredication, lockIdPredicate)
|
||||
val update = session.createQuery(criteriaUpdate).executeUpdate()
|
||||
if (update > 0) {
|
||||
log.trace("Releasing $update soft locked states for $lockId")
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
session.withTransaction(transactionIsolationLevel) {
|
||||
val updatedRows = update(VaultStatesEntity::class)
|
||||
.set(VaultStatesEntity.LOCK_ID, null)
|
||||
.set(VaultStatesEntity.LOCK_UPDATE_TIME, services.clock.instant())
|
||||
.where(VaultStatesEntity.STATE_STATUS eq Vault.StateStatus.UNCONSUMED)
|
||||
.and(VaultStatesEntity.LOCK_ID eq lockId.toString())
|
||||
.and(stateRefCompositeColumn.`in`(stateRefArgs(stateRefs))).get().value()
|
||||
if (updatedRows > 0) {
|
||||
log.trace("Releasing $updatedRows soft locked states for $lockId and stateRefs $stateRefs")
|
||||
}
|
||||
val criteriaUpdate = criteriaBuilder.createCriteriaUpdate(VaultSchemaV1.VaultStates::class.java)
|
||||
val vaultStates = criteriaUpdate.from(VaultSchemaV1.VaultStates::class.java)
|
||||
val stateStatusPredication = criteriaBuilder.equal(vaultStates.get<Vault.StateStatus>(VaultSchemaV1.VaultStates::stateStatus.name), Vault.StateStatus.UNCONSUMED)
|
||||
val lockIdPredicate = criteriaBuilder.equal(vaultStates.get<String>(VaultSchemaV1.VaultStates::lockId.name), lockId.toString())
|
||||
val persistentStateRefs = stateRefs.map { PersistentStateRef(it.txhash.bytes.toHexString(), it.index) }
|
||||
val compositeKey = vaultStates.get<PersistentStateRef>(VaultSchemaV1.VaultStates::stateRef.name)
|
||||
val stateRefsPredicate = criteriaBuilder.and(compositeKey.`in`(persistentStateRefs))
|
||||
criteriaUpdate.set<String>(vaultStates.get<String>(VaultSchemaV1.VaultStates::lockId.name), criteriaBuilder.nullLiteral(String::class.java))
|
||||
criteriaUpdate.set(vaultStates.get<Instant>(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp)
|
||||
criteriaUpdate.where(stateStatusPredication, lockIdPredicate, stateRefsPredicate)
|
||||
val updatedRows = session.createQuery(criteriaUpdate).executeUpdate()
|
||||
if (updatedRows > 0) {
|
||||
log.trace("Releasing $updatedRows soft locked states for $lockId and stateRefs $stateRefs")
|
||||
}
|
||||
} catch (e: PersistenceException) {
|
||||
} catch (e: Exception) {
|
||||
log.error("""soft lock update error attempting to release states for $lockId and $stateRefs")
|
||||
$e.
|
||||
""")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -462,11 +464,4 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P
|
||||
is LinearState -> state.isRelevant(ourKeys)
|
||||
else -> ourKeys.intersect(state.participants.map { it.owningKey }).isNotEmpty()
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to generate a string formatted list of Composite Keys for Requery Expression clause
|
||||
*/
|
||||
private fun stateRefArgs(stateRefs: Iterable<StateRef>): List<List<Any>> {
|
||||
return stateRefs.map { listOf("'${it.txhash}'", it.index) }
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,9 @@ import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.schemas.PersistentState
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import org.hibernate.annotations.Generated
|
||||
import org.hibernate.annotations.GenerationTime
|
||||
import java.io.Serializable
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import javax.persistence.*
|
||||
@ -22,7 +25,7 @@ object VaultSchema
|
||||
*/
|
||||
@CordaSerializable
|
||||
object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, version = 1,
|
||||
mappedTypes = listOf(VaultStates::class.java, VaultLinearStates::class.java, VaultFungibleStates::class.java)) {
|
||||
mappedTypes = listOf(VaultStates::class.java, VaultLinearStates::class.java, VaultFungibleStates::class.java, VaultTxnNote::class.java)) {
|
||||
@Entity
|
||||
@Table(name = "vault_states",
|
||||
indexes = arrayOf(Index(name = "state_status_idx", columnList = "state_status")))
|
||||
@ -50,16 +53,16 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio
|
||||
|
||||
/** refers to timestamp recorded upon entering CONSUMED state */
|
||||
@Column(name = "consumed_timestamp", nullable = true)
|
||||
var consumedTime: Instant?,
|
||||
var consumedTime: Instant? = null,
|
||||
|
||||
/** used to denote a state has been soft locked (to prevent double spend)
|
||||
* will contain a temporary unique [UUID] obtained from a flow session */
|
||||
@Column(name = "lock_id", nullable = true)
|
||||
var lockId: String,
|
||||
var lockId: String? = null,
|
||||
|
||||
/** refers to the last time a lock was taken (reserved) or updated (released, re-reserved) */
|
||||
@Column(name = "lock_timestamp", nullable = true)
|
||||
var lockUpdateTime: Instant?
|
||||
var lockUpdateTime: Instant? = null
|
||||
) : PersistentState()
|
||||
|
||||
@Entity
|
||||
@ -133,4 +136,23 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio
|
||||
issuerRef = _issuerRef.bytes,
|
||||
participants = _participants.toMutableSet())
|
||||
}
|
||||
|
||||
@Entity
|
||||
@Table(name = "vault_transaction_notes",
|
||||
indexes = arrayOf(Index(name = "seq_no_index", columnList = "seq_no"),
|
||||
Index(name = "transaction_id_index", columnList = "transaction_id")))
|
||||
class VaultTxnNote(
|
||||
@Id
|
||||
@GeneratedValue
|
||||
@Column(name = "seq_no")
|
||||
var seqNo: Int,
|
||||
|
||||
@Column(name = "transaction_id", length = 64)
|
||||
var txId: String,
|
||||
|
||||
@Column(name = "note")
|
||||
var note: String
|
||||
) : Serializable {
|
||||
constructor(txId: String, note: String) : this(0, txId, note)
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
package net.corda.node.services.vault;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import kotlin.Pair;
|
||||
import kotlin.*;
|
||||
import net.corda.contracts.DealState;
|
||||
import net.corda.contracts.asset.Cash;
|
||||
import net.corda.contracts.asset.CashUtilities;
|
||||
@ -19,7 +19,7 @@ import net.corda.core.node.services.vault.QueryCriteria.VaultCustomQueryCriteria
|
||||
import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria;
|
||||
import net.corda.core.schemas.MappedSchema;
|
||||
import net.corda.core.utilities.OpaqueBytes;
|
||||
import net.corda.node.utilities.CordaPersistence;
|
||||
import net.corda.node.utilities.*;
|
||||
import net.corda.schemas.CashSchemaV1;
|
||||
import net.corda.testing.TestConstants;
|
||||
import net.corda.testing.TestDependencyInjectionBase;
|
||||
@ -66,6 +66,7 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase {
|
||||
Set<MappedSchema> requiredSchemas = new HashSet<>();
|
||||
requiredSchemas.add(CashSchemaV1.INSTANCE);
|
||||
IdentityService identitySvc = makeTestIdentityService();
|
||||
@SuppressWarnings("unchecked")
|
||||
Pair<CordaPersistence, MockServices> databaseAndServices = makeTestDatabaseAndMockServices(requiredSchemas, keys, () -> identitySvc);
|
||||
issuerServices = new MockServices(getDUMMY_CASH_ISSUER_KEY(), getBOC_KEY());
|
||||
database = databaseAndServices.getFirst();
|
||||
@ -89,9 +90,10 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase {
|
||||
@Test
|
||||
public void unconsumedLinearStates() throws VaultQueryException {
|
||||
database.transaction(tx -> {
|
||||
|
||||
VaultFiller.fillWithSomeTestLinearStates(services, 3);
|
||||
|
||||
return tx;
|
||||
});
|
||||
database.transaction(tx -> {
|
||||
// DOCSTART VaultJavaQueryExample0
|
||||
Vault.Page<LinearState> results = vaultQuerySvc.queryBy(LinearState.class);
|
||||
// DOCEND VaultJavaQueryExample0
|
||||
@ -104,11 +106,12 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase {
|
||||
|
||||
@Test
|
||||
public void unconsumedStatesForStateRefsSortedByTxnId() {
|
||||
Vault<LinearState> issuedStates =
|
||||
database.transaction(tx -> {
|
||||
VaultFiller.fillWithSomeTestLinearStates(services, 8);
|
||||
return VaultFiller.fillWithSomeTestLinearStates(services, 2);
|
||||
});
|
||||
database.transaction(tx -> {
|
||||
|
||||
VaultFiller.fillWithSomeTestLinearStates(services, 8);
|
||||
Vault<LinearState> issuedStates = VaultFiller.fillWithSomeTestLinearStates(services, 2);
|
||||
|
||||
Stream<StateRef> stateRefsStream = StreamSupport.stream(issuedStates.getStates().spliterator(), false).map(StateAndRef::getRef);
|
||||
List<StateRef> stateRefs = stateRefsStream.collect(Collectors.toList());
|
||||
|
||||
@ -129,13 +132,11 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase {
|
||||
|
||||
@Test
|
||||
public void consumedCashStates() {
|
||||
Amount<Currency> amount = new Amount<>(100, Currency.getInstance("USD"));
|
||||
database.transaction(tx -> {
|
||||
|
||||
Amount<Currency> amount = new Amount<>(100, Currency.getInstance("USD"));
|
||||
|
||||
VaultFiller.fillWithSomeTestCash(services,
|
||||
new Amount<Currency>(100, Currency.getInstance("USD")),
|
||||
issuerServices,
|
||||
issuerServices,
|
||||
TestConstants.getDUMMY_NOTARY(),
|
||||
3,
|
||||
3,
|
||||
@ -143,9 +144,13 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase {
|
||||
new OpaqueBytes("1".getBytes()),
|
||||
null,
|
||||
CashUtilities.getDUMMY_CASH_ISSUER());
|
||||
|
||||
return tx;
|
||||
});
|
||||
database.transaction(tx -> {
|
||||
VaultFiller.consumeCash(services, amount, getDUMMY_NOTARY());
|
||||
|
||||
return tx;
|
||||
});
|
||||
database.transaction(tx -> {
|
||||
// DOCSTART VaultJavaQueryExample1
|
||||
VaultQueryCriteria criteria = new VaultQueryCriteria(Vault.StateStatus.CONSUMED);
|
||||
Vault.Page<Cash.State> results = vaultQuerySvc.queryBy(Cash.State.class, criteria);
|
||||
@ -159,19 +164,24 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase {
|
||||
|
||||
@Test
|
||||
public void consumedDealStatesPagedSorted() throws VaultQueryException {
|
||||
List<String> dealIds = Arrays.asList("123", "456", "789");
|
||||
@SuppressWarnings("unchecked")
|
||||
Triple<StateAndRef<LinearState>, UniqueIdentifier, Vault<DealState>> ids =
|
||||
database.transaction((DatabaseTransaction tx) -> {
|
||||
Vault<LinearState> states = VaultFiller.fillWithSomeTestLinearStates(services, 10, null);
|
||||
StateAndRef<LinearState> linearState = states.getStates().iterator().next();
|
||||
UniqueIdentifier uid = linearState.component1().getData().getLinearId();
|
||||
|
||||
Vault<DealState> dealStates = VaultFiller.fillWithSomeTestDeals(services, dealIds);
|
||||
return new Triple(linearState,uid,dealStates);
|
||||
});
|
||||
database.transaction(tx -> {
|
||||
|
||||
Vault<LinearState> states = VaultFiller.fillWithSomeTestLinearStates(services, 10, null);
|
||||
StateAndRef<LinearState> linearState = states.getStates().iterator().next();
|
||||
UniqueIdentifier uid = linearState.component1().getData().getLinearId();
|
||||
|
||||
List<String> dealIds = Arrays.asList("123", "456", "789");
|
||||
Vault<DealState> dealStates = VaultFiller.fillWithSomeTestDeals(services, dealIds);
|
||||
|
||||
// consume states
|
||||
VaultFiller.consumeDeals(services, (List<? extends StateAndRef<? extends DealState>>) dealStates.getStates(), getDUMMY_NOTARY());
|
||||
VaultFiller.consumeLinearStates(services, Collections.singletonList(linearState), getDUMMY_NOTARY());
|
||||
|
||||
VaultFiller.consumeDeals(services, (List<? extends StateAndRef<? extends DealState>>) ids.getThird().getStates(), getDUMMY_NOTARY());
|
||||
VaultFiller.consumeLinearStates(services, Collections.singletonList(ids.getFirst()), getDUMMY_NOTARY());
|
||||
return tx;
|
||||
});
|
||||
database.transaction(tx -> {
|
||||
// DOCSTART VaultJavaQueryExample2
|
||||
Vault.StateStatus status = Vault.StateStatus.CONSUMED;
|
||||
@SuppressWarnings("unchecked")
|
||||
@ -179,7 +189,7 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase {
|
||||
|
||||
QueryCriteria vaultCriteria = new VaultQueryCriteria(status, contractStateTypes);
|
||||
|
||||
List<UUID> linearIds = Collections.singletonList(uid.getId());
|
||||
List<UUID> linearIds = Collections.singletonList(ids.getSecond().getId());
|
||||
QueryCriteria linearCriteriaAll = new LinearStateQueryCriteria(null, linearIds);
|
||||
QueryCriteria dealCriteriaAll = new LinearStateQueryCriteria(null, null, dealIds);
|
||||
|
||||
@ -212,7 +222,9 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase {
|
||||
VaultFiller.fillWithSomeTestCash(services, dollars100, issuerServices, TestConstants.getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER());
|
||||
VaultFiller.fillWithSomeTestCash(services, dollars10, issuerServices, TestConstants.getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER());
|
||||
VaultFiller.fillWithSomeTestCash(services, dollars1, issuerServices, TestConstants.getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER());
|
||||
|
||||
return tx;
|
||||
});
|
||||
database.transaction(tx -> {
|
||||
try {
|
||||
// DOCSTART VaultJavaQueryExample3
|
||||
QueryCriteria generalCriteria = new VaultQueryCriteria(Vault.StateStatus.ALL);
|
||||
@ -256,7 +268,9 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase {
|
||||
new OpaqueBytes("1".getBytes()),
|
||||
null,
|
||||
getDUMMY_CASH_ISSUER());
|
||||
|
||||
return tx;
|
||||
});
|
||||
database.transaction(tx -> {
|
||||
// DOCSTART VaultJavaQueryExample4
|
||||
@SuppressWarnings("unchecked")
|
||||
Set<Class<ContractState>> contractStateTypes = new HashSet(Collections.singletonList(Cash.State.class));
|
||||
@ -276,14 +290,16 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase {
|
||||
|
||||
@Test
|
||||
public void trackDealStatesPagedSorted() {
|
||||
List<String> dealIds = Arrays.asList("123", "456", "789");
|
||||
UniqueIdentifier uid =
|
||||
database.transaction(tx -> {
|
||||
Vault<LinearState> states = VaultFiller.fillWithSomeTestLinearStates(services, 10, null);
|
||||
UniqueIdentifier _uid = states.getStates().iterator().next().component1().getData().getLinearId();
|
||||
|
||||
VaultFiller.fillWithSomeTestDeals(services, dealIds);
|
||||
return _uid;
|
||||
});
|
||||
database.transaction(tx -> {
|
||||
|
||||
Vault<LinearState> states = VaultFiller.fillWithSomeTestLinearStates(services, 10, null);
|
||||
UniqueIdentifier uid = states.getStates().iterator().next().component1().getData().getLinearId();
|
||||
|
||||
List<String> dealIds = Arrays.asList("123", "456", "789");
|
||||
VaultFiller.fillWithSomeTestDeals(services, dealIds);
|
||||
|
||||
// DOCSTART VaultJavaQueryExample5
|
||||
@SuppressWarnings("unchecked")
|
||||
Set<Class<ContractState>> contractStateTypes = new HashSet(Arrays.asList(DealState.class, LinearState.class));
|
||||
@ -331,6 +347,9 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase {
|
||||
VaultFiller.fillWithSomeTestCash(services, pounds, issuerServices, TestConstants.getDUMMY_NOTARY(), 4, 4, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER());
|
||||
VaultFiller.fillWithSomeTestCash(services, swissfrancs, issuerServices, TestConstants.getDUMMY_NOTARY(), 5, 5, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER());
|
||||
|
||||
return tx;
|
||||
});
|
||||
database.transaction(tx -> {
|
||||
try {
|
||||
// DOCSTART VaultJavaQueryExample21
|
||||
Field pennies = CashSchemaV1.PersistentCashState.class.getDeclaredField("pennies");
|
||||
@ -376,6 +395,9 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase {
|
||||
VaultFiller.fillWithSomeTestCash(services, pounds, issuerServices, TestConstants.getDUMMY_NOTARY(), 4, 4, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER());
|
||||
VaultFiller.fillWithSomeTestCash(services, swissfrancs, issuerServices, TestConstants.getDUMMY_NOTARY(), 5, 5, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER());
|
||||
|
||||
return tx;
|
||||
});
|
||||
database.transaction(tx -> {
|
||||
try {
|
||||
// DOCSTART VaultJavaQueryExample22
|
||||
Field pennies = CashSchemaV1.PersistentCashState.class.getDeclaredField("pennies");
|
||||
@ -434,7 +456,6 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase {
|
||||
@SuppressWarnings("unchecked")
|
||||
public void aggregateFunctionsSumByIssuerAndCurrencyAndSortByAggregateSum() {
|
||||
database.transaction(tx -> {
|
||||
|
||||
Amount<Currency> dollars100 = new Amount<>(100, Currency.getInstance("USD"));
|
||||
Amount<Currency> dollars200 = new Amount<>(200, Currency.getInstance("USD"));
|
||||
Amount<Currency> pounds300 = new Amount<>(300, Currency.getInstance("GBP"));
|
||||
@ -445,6 +466,9 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase {
|
||||
VaultFiller.fillWithSomeTestCash(services, pounds300, issuerServices, TestConstants.getDUMMY_NOTARY(), 3, 3, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER());
|
||||
VaultFiller.fillWithSomeTestCash(services, pounds400, issuerServices, TestConstants.getDUMMY_NOTARY(), 4, 4, new Random(0L), new OpaqueBytes("1".getBytes()), null, getBOC().ref(new OpaqueBytes("1".getBytes())));
|
||||
|
||||
return tx;
|
||||
});
|
||||
database.transaction(tx -> {
|
||||
try {
|
||||
// DOCSTART VaultJavaQueryExample23
|
||||
Field pennies = CashSchemaV1.PersistentCashState.class.getDeclaredField("pennies");
|
||||
|
@ -74,7 +74,7 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() {
|
||||
database.transaction {
|
||||
hibernateConfig = HibernateConfiguration(NodeSchemaService(customSchemas), makeTestDatabaseProperties(), ::makeTestIdentityService)
|
||||
services = object : MockServices(BOB_KEY, BOC_KEY, DUMMY_NOTARY_KEY) {
|
||||
override val vaultService: VaultService = makeVaultService(dataSourceProps, hibernateConfig)
|
||||
override val vaultService: VaultService = makeVaultService(hibernateConfig)
|
||||
|
||||
override fun recordTransactions(notifyVault: Boolean, txs: Iterable<SignedTransaction>) {
|
||||
for (stx in txs) {
|
||||
|
@ -1,213 +0,0 @@
|
||||
package net.corda.node.services.database
|
||||
|
||||
import io.requery.Persistable
|
||||
import io.requery.kotlin.eq
|
||||
import io.requery.sql.KotlinEntityDataStore
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.node.services.persistence.DBTransactionStorage
|
||||
import net.corda.node.services.vault.schemas.requery.Models
|
||||
import net.corda.node.services.vault.schemas.requery.VaultCashBalancesEntity
|
||||
import net.corda.node.services.vault.schemas.requery.VaultSchema
|
||||
import net.corda.node.services.vault.schemas.requery.VaultStatesEntity
|
||||
import net.corda.node.utilities.CordaPersistence
|
||||
import net.corda.node.utilities.configureDatabase
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.dummyCommand
|
||||
import net.corda.testing.node.makeTestDataSourceProperties
|
||||
import net.corda.testing.node.makeTestDatabaseProperties
|
||||
import net.corda.testing.node.makeTestIdentityService
|
||||
import org.assertj.core.api.Assertions
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
class RequeryConfigurationTest : TestDependencyInjectionBase() {
|
||||
|
||||
lateinit var database: CordaPersistence
|
||||
lateinit var transactionStorage: DBTransactionStorage
|
||||
lateinit var requerySession: KotlinEntityDataStore<Persistable>
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
val dataSourceProperties = makeTestDataSourceProperties()
|
||||
database = configureDatabase(dataSourceProperties, makeTestDatabaseProperties(), identitySvc = ::makeTestIdentityService)
|
||||
newTransactionStorage()
|
||||
newRequeryStorage(dataSourceProperties)
|
||||
}
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
database.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `transaction inserts in same DB transaction scope across two persistence engines`() {
|
||||
val txn = newTransaction()
|
||||
|
||||
database.transaction {
|
||||
transactionStorage.addTransaction(txn)
|
||||
requerySession.withTransaction {
|
||||
insert(createVaultStateEntity(txn))
|
||||
}
|
||||
}
|
||||
|
||||
database.transaction {
|
||||
Assertions.assertThat(transactionStorage.transactions).containsOnly(txn)
|
||||
requerySession.withTransaction {
|
||||
val result = select(VaultSchema.VaultStates::class) where (VaultSchema.VaultStates::txId eq txn.tx.inputs[0].txhash.toString())
|
||||
Assertions.assertThat(result.get().first().txId).isEqualTo(txn.tx.inputs[0].txhash.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `transaction operations in same DB transaction scope across two persistence engines`() {
|
||||
val txn = newTransaction()
|
||||
|
||||
database.transaction {
|
||||
transactionStorage.addTransaction(txn)
|
||||
requerySession.withTransaction {
|
||||
upsert(createCashBalance())
|
||||
select(VaultSchema.VaultCashBalances::class).get().first()
|
||||
insert(createVaultStateEntity(txn))
|
||||
}
|
||||
}
|
||||
|
||||
database.transaction {
|
||||
Assertions.assertThat(transactionStorage.transactions).containsOnly(txn)
|
||||
requerySession.withTransaction {
|
||||
val cashQuery = select(VaultSchema.VaultCashBalances::class) where (VaultSchema.VaultCashBalances::currency eq "GBP")
|
||||
assertEquals(12345, cashQuery.get().first().amount)
|
||||
val stateQuery = select(VaultSchema.VaultStates::class) where (VaultSchema.VaultStates::txId eq txn.tx.inputs[0].txhash.toString())
|
||||
Assertions.assertThat(stateQuery.get().first().txId).isEqualTo(txn.tx.inputs[0].txhash.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `transaction rollback in same DB transaction scope across two persistence engines`() {
|
||||
val txn = newTransaction()
|
||||
|
||||
database.transaction {
|
||||
transactionStorage.addTransaction(txn)
|
||||
requerySession.withTransaction {
|
||||
insert(createVaultStateEntity(txn))
|
||||
}
|
||||
rollback()
|
||||
}
|
||||
|
||||
database.transaction {
|
||||
Assertions.assertThat(transactionStorage.transactions).isEmpty()
|
||||
requerySession.withTransaction {
|
||||
val result = select(VaultSchema.VaultStates::class) where (VaultSchema.VaultStates::txId eq txn.tx.inputs[0].txhash.toString())
|
||||
Assertions.assertThat(result.get().count() == 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `bounded iteration`() {
|
||||
// insert 100 entities
|
||||
database.transaction {
|
||||
requerySession.withTransaction {
|
||||
(1..100)
|
||||
.map { newTransaction(it) }
|
||||
.forEach { insert(createVaultStateEntity(it)) }
|
||||
}
|
||||
}
|
||||
|
||||
// query entities 41..45
|
||||
database.transaction {
|
||||
requerySession.withTransaction {
|
||||
// Note: cannot specify a limit explicitly when using iterator skip & take
|
||||
val query = select(VaultSchema.VaultStates::class)
|
||||
val count = query.get().count()
|
||||
Assertions.assertThat(count).isEqualTo(100)
|
||||
val result = query.get().iterator(40, 5)
|
||||
Assertions.assertThat(result.asSequence().count()).isEqualTo(5)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test calling an arbitrary JDBC native query`() {
|
||||
val txn = newTransaction()
|
||||
|
||||
database.transaction {
|
||||
transactionStorage.addTransaction(txn)
|
||||
requerySession.withTransaction {
|
||||
insert(createVaultStateEntity(txn))
|
||||
}
|
||||
}
|
||||
|
||||
val dataSourceProperties = makeTestDataSourceProperties()
|
||||
val nativeQuery = "SELECT v.transaction_id, v.output_index FROM vault_states v WHERE v.state_status = 0"
|
||||
|
||||
database.transaction {
|
||||
val configuration = RequeryConfiguration(dataSourceProperties, true, makeTestDatabaseProperties())
|
||||
val jdbcSession = configuration.jdbcSession()
|
||||
val prepStatement = jdbcSession.prepareStatement(nativeQuery)
|
||||
val rs = prepStatement.executeQuery()
|
||||
assertTrue(rs.next())
|
||||
assertEquals(rs.getString(1), txn.tx.inputs[0].txhash.toString())
|
||||
assertEquals(rs.getInt(2), txn.tx.inputs[0].index)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createVaultStateEntity(txn: SignedTransaction): VaultStatesEntity {
|
||||
val txnState = txn.tx.inputs[0]
|
||||
val state = VaultStatesEntity().apply {
|
||||
txId = txnState.txhash.toString()
|
||||
index = txnState.index
|
||||
stateStatus = Vault.StateStatus.UNCONSUMED
|
||||
contractStateClassName = DummyContract.SingleOwnerState::class.java.name
|
||||
contractState = DummyContract.SingleOwnerState(owner = AnonymousParty(MEGA_CORP_PUBKEY)).serialize().bytes
|
||||
notaryName = txn.tx.notary!!.name.toString()
|
||||
recordedTime = Instant.now()
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
private fun createCashBalance(): VaultCashBalancesEntity {
|
||||
val cashBalanceEntity = VaultCashBalancesEntity()
|
||||
cashBalanceEntity.currency = "GBP"
|
||||
cashBalanceEntity.amount = 12345
|
||||
return cashBalanceEntity
|
||||
}
|
||||
|
||||
private fun newTransactionStorage() {
|
||||
database.transaction {
|
||||
transactionStorage = DBTransactionStorage()
|
||||
}
|
||||
}
|
||||
|
||||
private fun newRequeryStorage(dataSourceProperties: Properties) {
|
||||
database.transaction {
|
||||
val configuration = RequeryConfiguration(dataSourceProperties, true, makeTestDatabaseProperties())
|
||||
requerySession = configuration.sessionForModel(Models.VAULT)
|
||||
}
|
||||
}
|
||||
|
||||
private fun newTransaction(index: Int = 0): SignedTransaction {
|
||||
val wtx = WireTransaction(
|
||||
inputs = listOf(StateRef(SecureHash.randomSHA256(), index)),
|
||||
attachments = emptyList(),
|
||||
outputs = emptyList(),
|
||||
commands = listOf(dummyCommand()),
|
||||
notary = DUMMY_NOTARY,
|
||||
timeWindow = null
|
||||
)
|
||||
return SignedTransaction(wtx, listOf(TransactionSignature(ByteArray(1), ALICE_PUBKEY, SignatureMetadata(1, Crypto.findSignatureScheme(ALICE_PUBKEY).schemeNumberID))))
|
||||
}
|
||||
}
|
@ -11,8 +11,10 @@ import net.corda.core.node.services.VaultService
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.node.services.MockServiceHubInternal
|
||||
import net.corda.node.services.database.HibernateConfiguration
|
||||
import net.corda.node.services.identity.InMemoryIdentityService
|
||||
import net.corda.node.services.persistence.DBCheckpointStorage
|
||||
import net.corda.node.services.schema.NodeSchemaService
|
||||
import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl
|
||||
import net.corda.node.services.statemachine.StateMachineManager
|
||||
import net.corda.node.services.vault.NodeVaultService
|
||||
@ -71,7 +73,8 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() {
|
||||
smmHasRemovedAllFlows = CountDownLatch(1)
|
||||
calls = 0
|
||||
val dataSourceProps = makeTestDataSourceProperties()
|
||||
database = configureDatabase(dataSourceProps, makeTestDatabaseProperties(), identitySvc = ::makeTestIdentityService)
|
||||
val databaseProperties = makeTestDatabaseProperties()
|
||||
database = configureDatabase(dataSourceProps, databaseProperties, identitySvc = ::makeTestIdentityService)
|
||||
val identityService = InMemoryIdentityService(trustRoot = DUMMY_CA.certificate)
|
||||
val kms = MockKeyManagementService(identityService, ALICE_KEY)
|
||||
|
||||
@ -88,7 +91,7 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() {
|
||||
overrideClock = testClock,
|
||||
keyManagement = kms,
|
||||
network = mockMessagingService), TestReference {
|
||||
override val vaultService: VaultService = NodeVaultService(this, dataSourceProps, makeTestDatabaseProperties())
|
||||
override val vaultService: VaultService = NodeVaultService(this)
|
||||
override val testReference = this@NodeSchedulerServiceTest
|
||||
}
|
||||
smmExecutor = AffinityExecutor.ServiceAffinityExecutor("test", 1)
|
||||
|
@ -59,7 +59,7 @@ class DBTransactionStorageTests : TestDependencyInjectionBase() {
|
||||
|
||||
services = object : MockServices(BOB_KEY) {
|
||||
override val vaultService: VaultService get() {
|
||||
val vaultService = NodeVaultService(this, dataSourceProps, makeTestDatabaseProperties())
|
||||
val vaultService = NodeVaultService(this)
|
||||
hibernatePersister = HibernateObserver(vaultService.rawUpdates, hibernateConfig)
|
||||
return vaultService
|
||||
}
|
||||
|
@ -7,14 +7,13 @@ import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.internal.read
|
||||
import net.corda.core.internal.readAll
|
||||
import net.corda.testing.LogHelper
|
||||
import net.corda.core.internal.write
|
||||
import net.corda.core.internal.writeLines
|
||||
import net.corda.node.services.database.RequeryConfiguration
|
||||
import net.corda.node.services.persistence.schemas.requery.AttachmentEntity
|
||||
import net.corda.node.services.transactions.PersistentUniquenessProvider
|
||||
import net.corda.node.utilities.CordaPersistence
|
||||
import net.corda.node.utilities.DatabaseTransactionManager
|
||||
import net.corda.node.utilities.configureDatabase
|
||||
import net.corda.testing.LogHelper
|
||||
import net.corda.testing.node.makeTestDataSourceProperties
|
||||
import net.corda.testing.node.makeTestDatabaseProperties
|
||||
import net.corda.testing.node.makeTestIdentityService
|
||||
@ -25,7 +24,6 @@ import java.nio.charset.Charset
|
||||
import java.nio.file.FileAlreadyExistsException
|
||||
import java.nio.file.FileSystem
|
||||
import java.nio.file.Path
|
||||
import java.util.*
|
||||
import java.util.jar.JarEntry
|
||||
import java.util.jar.JarOutputStream
|
||||
import kotlin.test.assertEquals
|
||||
@ -36,17 +34,13 @@ class NodeAttachmentStorageTest {
|
||||
// Use an in memory file system for testing attachment storage.
|
||||
lateinit var fs: FileSystem
|
||||
lateinit var database: CordaPersistence
|
||||
lateinit var dataSourceProperties: Properties
|
||||
lateinit var configuration: RequeryConfiguration
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
LogHelper.setLevel(PersistentUniquenessProvider::class)
|
||||
|
||||
dataSourceProperties = makeTestDataSourceProperties()
|
||||
val dataSourceProperties = makeTestDataSourceProperties()
|
||||
database = configureDatabase(dataSourceProperties, makeTestDatabaseProperties(), identitySvc = ::makeTestIdentityService)
|
||||
|
||||
configuration = RequeryConfiguration(dataSourceProperties, databaseProperties = makeTestDatabaseProperties())
|
||||
fs = Jimfs.newFileSystem(Configuration.unix())
|
||||
}
|
||||
|
||||
@ -61,7 +55,7 @@ class NodeAttachmentStorageTest {
|
||||
val expectedHash = testJar.readAll().sha256()
|
||||
|
||||
database.transaction {
|
||||
val storage = NodeAttachmentService(dataSourceProperties, MetricRegistry(), makeTestDatabaseProperties())
|
||||
val storage = NodeAttachmentService(MetricRegistry())
|
||||
val id = testJar.read { storage.importAttachment(it) }
|
||||
assertEquals(expectedHash, id)
|
||||
|
||||
@ -87,7 +81,7 @@ class NodeAttachmentStorageTest {
|
||||
fun `duplicates not allowed`() {
|
||||
val testJar = makeTestJar()
|
||||
database.transaction {
|
||||
val storage = NodeAttachmentService(dataSourceProperties, MetricRegistry(), makeTestDatabaseProperties())
|
||||
val storage = NodeAttachmentService(MetricRegistry())
|
||||
testJar.read {
|
||||
storage.importAttachment(it)
|
||||
}
|
||||
@ -102,19 +96,21 @@ class NodeAttachmentStorageTest {
|
||||
@Test
|
||||
fun `corrupt entry throws exception`() {
|
||||
val testJar = makeTestJar()
|
||||
val id =
|
||||
database.transaction {
|
||||
val storage = NodeAttachmentService(dataSourceProperties, MetricRegistry(), makeTestDatabaseProperties())
|
||||
val storage = NodeAttachmentService(MetricRegistry())
|
||||
val id = testJar.read { storage.importAttachment(it) }
|
||||
|
||||
// Corrupt the file in the store.
|
||||
val bytes = testJar.readAll()
|
||||
val corruptBytes = "arggghhhh".toByteArray()
|
||||
System.arraycopy(corruptBytes, 0, bytes, 0, corruptBytes.size)
|
||||
val corruptAttachment = AttachmentEntity()
|
||||
corruptAttachment.attId = id
|
||||
corruptAttachment.content = bytes
|
||||
storage.session.update(corruptAttachment)
|
||||
|
||||
val corruptAttachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = bytes)
|
||||
DatabaseTransactionManager.current().session.merge(corruptAttachment)
|
||||
id
|
||||
}
|
||||
database.transaction {
|
||||
val storage = NodeAttachmentService(MetricRegistry())
|
||||
val e = assertFailsWith<NodeAttachmentService.HashMismatchException> {
|
||||
storage.openAttachment(id)!!.open().use { it.readBytes() }
|
||||
}
|
||||
@ -131,7 +127,7 @@ class NodeAttachmentStorageTest {
|
||||
@Test
|
||||
fun `non jar rejected`() {
|
||||
database.transaction {
|
||||
val storage = NodeAttachmentService(dataSourceProperties, MetricRegistry(), makeTestDatabaseProperties())
|
||||
val storage = NodeAttachmentService(MetricRegistry())
|
||||
val path = fs.getPath("notajar")
|
||||
path.writeLines(listOf("Hey", "there!"))
|
||||
path.read {
|
||||
|
@ -84,9 +84,9 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
|
||||
@Test
|
||||
fun `states not local to instance`() {
|
||||
database.transaction {
|
||||
|
||||
services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 3, 3, Random(0L))
|
||||
|
||||
}
|
||||
database.transaction {
|
||||
val w1 = vaultQuery.queryBy<Cash.State>().states
|
||||
assertThat(w1).hasSize(3)
|
||||
|
||||
@ -112,9 +112,9 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
|
||||
@Test
|
||||
fun `states for refs`() {
|
||||
database.transaction {
|
||||
|
||||
services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 3, 3, Random(0L))
|
||||
|
||||
}
|
||||
database.transaction {
|
||||
val w1 = vaultQuery.queryBy<Cash.State>().states
|
||||
assertThat(w1).hasSize(3)
|
||||
|
||||
@ -126,8 +126,9 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
|
||||
@Test
|
||||
fun `states soft locking reserve and release`() {
|
||||
database.transaction {
|
||||
|
||||
services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 3, 3, Random(0L))
|
||||
}
|
||||
database.transaction {
|
||||
|
||||
val unconsumedStates = vaultQuery.queryBy<Cash.State>().states
|
||||
assertThat(unconsumedStates).hasSize(3)
|
||||
@ -309,8 +310,9 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
|
||||
@Test
|
||||
fun `unconsumedStatesForSpending exact amount`() {
|
||||
database.transaction {
|
||||
|
||||
services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 1, 1, Random(0L))
|
||||
}
|
||||
database.transaction {
|
||||
|
||||
val unconsumedStates = vaultQuery.queryBy<Cash.State>().states
|
||||
assertThat(unconsumedStates).hasSize(1)
|
||||
@ -327,10 +329,10 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
|
||||
@Test
|
||||
fun `unconsumedStatesForSpending from two issuer parties`() {
|
||||
database.transaction {
|
||||
|
||||
services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (DUMMY_CASH_ISSUER))
|
||||
services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(1)))
|
||||
|
||||
}
|
||||
database.transaction {
|
||||
val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(200.DOLLARS,
|
||||
onlyFromIssuerParties = setOf(DUMMY_CASH_ISSUER.party, BOC))
|
||||
spendableStatesUSD.forEach(::println)
|
||||
@ -344,12 +346,12 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
|
||||
@Test
|
||||
fun `unconsumedStatesForSpending from specific issuer party and refs`() {
|
||||
database.transaction {
|
||||
|
||||
services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (DUMMY_CASH_ISSUER))
|
||||
services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(1)), ref = OpaqueBytes.of(1))
|
||||
services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(2)), ref = OpaqueBytes.of(2))
|
||||
services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(3)), ref = OpaqueBytes.of(3))
|
||||
|
||||
}
|
||||
database.transaction {
|
||||
val unconsumedStates = vaultQuery.queryBy<Cash.State>().states
|
||||
assertThat(unconsumedStates).hasSize(4)
|
||||
|
||||
@ -366,9 +368,9 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
|
||||
@Test
|
||||
fun `unconsumedStatesForSpending insufficient amount`() {
|
||||
database.transaction {
|
||||
|
||||
services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 1, 1, Random(0L))
|
||||
|
||||
}
|
||||
database.transaction {
|
||||
val unconsumedStates = vaultQuery.queryBy<Cash.State>().states
|
||||
assertThat(unconsumedStates).hasSize(1)
|
||||
|
||||
@ -383,9 +385,9 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
|
||||
@Test
|
||||
fun `unconsumedStatesForSpending small amount`() {
|
||||
database.transaction {
|
||||
|
||||
services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 2, 2, Random(0L))
|
||||
|
||||
}
|
||||
database.transaction {
|
||||
val unconsumedStates = vaultQuery.queryBy<Cash.State>().states
|
||||
assertThat(unconsumedStates).hasSize(2)
|
||||
|
||||
@ -401,11 +403,11 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
|
||||
@Test
|
||||
fun `states soft locking query granularity`() {
|
||||
database.transaction {
|
||||
|
||||
services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 10, 10, Random(0L))
|
||||
services.fillWithSomeTestCash(100.POUNDS, issuerServices, DUMMY_NOTARY, 10, 10, Random(0L))
|
||||
services.fillWithSomeTestCash(100.SWISS_FRANCS, issuerServices, DUMMY_NOTARY, 10, 10, Random(0L))
|
||||
|
||||
}
|
||||
database.transaction {
|
||||
var unlockedStates = 30
|
||||
val allStates = vaultQuery.queryBy<Cash.State>().states
|
||||
assertThat(allStates).hasSize(unlockedStates)
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -61,7 +61,8 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
|
||||
database.transaction {
|
||||
// Fix the PRNG so that we get the same splits every time.
|
||||
services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 3, 3, Random(0L), issuedBy = DUMMY_CASH_ISSUER)
|
||||
|
||||
}
|
||||
database.transaction {
|
||||
val w = vaultQuery.queryBy<Cash.State>().states
|
||||
assertEquals(3, w.size)
|
||||
|
||||
@ -77,25 +78,31 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
|
||||
@Test
|
||||
fun `issue and spend total correctly and irrelevant ignored`() {
|
||||
val megaCorpServices = MockServices(MEGA_CORP_KEY)
|
||||
val freshKey = services.keyManagementService.freshKey()
|
||||
|
||||
val usefulTX =
|
||||
database.transaction {
|
||||
// A tx that sends us money.
|
||||
val usefulBuilder = TransactionBuilder(null)
|
||||
Cash().generateIssue(usefulBuilder, 100.DOLLARS `issued by` MEGA_CORP.ref(1), AnonymousParty(freshKey), DUMMY_NOTARY)
|
||||
megaCorpServices.signInitialTransaction(usefulBuilder)
|
||||
}
|
||||
database.transaction {
|
||||
// A tx that sends us money.
|
||||
val freshKey = services.keyManagementService.freshKey()
|
||||
val usefulBuilder = TransactionBuilder(null)
|
||||
Cash().generateIssue(usefulBuilder, 100.DOLLARS `issued by` MEGA_CORP.ref(1), AnonymousParty(freshKey), DUMMY_NOTARY)
|
||||
val usefulTX = megaCorpServices.signInitialTransaction(usefulBuilder)
|
||||
|
||||
assertEquals(0.DOLLARS, services.getCashBalance(USD))
|
||||
services.recordTransactions(usefulTX)
|
||||
|
||||
// A tx that spends our money.
|
||||
val spendTXBuilder = TransactionBuilder(DUMMY_NOTARY)
|
||||
Cash.generateSpend(services, spendTXBuilder, 80.DOLLARS, BOB)
|
||||
val spendPTX = services.signInitialTransaction(spendTXBuilder, freshKey)
|
||||
val spendTX = notaryServices.addSignature(spendPTX)
|
||||
|
||||
}
|
||||
val spendTX =
|
||||
database.transaction {
|
||||
// A tx that spends our money.
|
||||
val spendTXBuilder = TransactionBuilder(DUMMY_NOTARY)
|
||||
Cash.generateSpend(services, spendTXBuilder, 80.DOLLARS, BOB)
|
||||
val spendPTX = services.signInitialTransaction(spendTXBuilder, freshKey)
|
||||
notaryServices.addSignature(spendPTX)
|
||||
}
|
||||
database.transaction {
|
||||
assertEquals(100.DOLLARS, services.getCashBalance(USD))
|
||||
|
||||
}
|
||||
database.transaction {
|
||||
// A tx that doesn't send us anything.
|
||||
val irrelevantBuilder = TransactionBuilder(DUMMY_NOTARY)
|
||||
Cash().generateIssue(irrelevantBuilder, 100.DOLLARS `issued by` MEGA_CORP.ref(1), BOB, DUMMY_NOTARY)
|
||||
@ -104,12 +111,15 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
|
||||
val irrelevantTX = notaryServices.addSignature(irrelevantPTX)
|
||||
|
||||
services.recordTransactions(irrelevantTX)
|
||||
}
|
||||
database.transaction {
|
||||
assertEquals(100.DOLLARS, services.getCashBalance(USD))
|
||||
}
|
||||
database.transaction {
|
||||
services.recordTransactions(spendTX)
|
||||
|
||||
}
|
||||
database.transaction {
|
||||
assertEquals(20.DOLLARS, services.getCashBalance(USD))
|
||||
|
||||
// TODO: Flesh out these tests as needed.
|
||||
}
|
||||
}
|
||||
|
||||
@ -123,7 +133,8 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
|
||||
services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 10, 10, Random(0L), ownedBy = AnonymousParty(freshKey),
|
||||
issuedBy = MEGA_CORP.ref(1))
|
||||
println("Cash balance: ${services.getCashBalance(USD)}")
|
||||
|
||||
}
|
||||
database.transaction {
|
||||
assertThat(vaultQuery.queryBy<Cash.State>().states).hasSize(10)
|
||||
assertThat(vaultQuery.queryBy<Cash.State>(criteriaLocked).states).hasSize(0)
|
||||
}
|
||||
@ -234,22 +245,23 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
|
||||
|
||||
@Test
|
||||
fun `sequencing LinearStates works`() {
|
||||
database.transaction {
|
||||
val freshKey = services.keyManagementService.freshKey()
|
||||
val freshIdentity = AnonymousParty(freshKey)
|
||||
val freshKey = services.keyManagementService.freshKey()
|
||||
val freshIdentity = AnonymousParty(freshKey)
|
||||
val linearId = UniqueIdentifier()
|
||||
|
||||
val linearId = UniqueIdentifier()
|
||||
|
||||
// Issue a linear state
|
||||
val dummyIssue =
|
||||
database.transaction { // Issue a linear state
|
||||
val dummyIssueBuilder = TransactionBuilder(notary = DUMMY_NOTARY)
|
||||
.addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshIdentity)))
|
||||
.addCommand(dummyCommand(notaryServices.legalIdentityKey))
|
||||
.addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshIdentity))).addCommand(dummyCommand(notaryServices.legalIdentityKey))
|
||||
val dummyIssuePtx = notaryServices.signInitialTransaction(dummyIssueBuilder)
|
||||
val dummyIssue = services.addSignature(dummyIssuePtx)
|
||||
|
||||
dummyIssue.toLedgerTransaction(services).verify()
|
||||
dummyIssue.toLedgerTransaction(services).verify()
|
||||
|
||||
services.recordTransactions(dummyIssue)
|
||||
services.recordTransactions(dummyIssue)
|
||||
dummyIssue
|
||||
}
|
||||
database.transaction {
|
||||
assertThat(vaultQuery.queryBy<DummyLinearContract.State>().states).hasSize(1)
|
||||
|
||||
// Move the same state
|
||||
@ -263,6 +275,8 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
|
||||
dummyIssue.toLedgerTransaction(services).verify()
|
||||
|
||||
services.recordTransactions(dummyMove)
|
||||
}
|
||||
database.transaction {
|
||||
assertThat(vaultQuery.queryBy<DummyLinearContract.State>().states).hasSize(1)
|
||||
}
|
||||
}
|
||||
@ -275,10 +289,15 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
|
||||
services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 3, 3, Random(0L), ownedBy = AnonymousParty(freshKey))
|
||||
services.fillWithSomeTestCash(100.SWISS_FRANCS, issuerServices, DUMMY_NOTARY, 2, 2, Random(0L))
|
||||
services.fillWithSomeTestCash(100.POUNDS, issuerServices, DUMMY_NOTARY, 1, 1, Random(0L))
|
||||
}
|
||||
database.transaction {
|
||||
val cash = vaultQuery.queryBy<Cash.State>().states
|
||||
cash.forEach { println(it.state.data.amount) }
|
||||
|
||||
}
|
||||
database.transaction {
|
||||
services.fillWithSomeTestDeals(listOf("123", "456", "789"))
|
||||
}
|
||||
database.transaction {
|
||||
val deals = vaultQuery.queryBy<DummyDealContract.State>().states
|
||||
deals.forEach { println(it.state.data.linearId.externalId!!) }
|
||||
}
|
||||
@ -290,7 +309,8 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
|
||||
val spendPTX = notaryServices.signInitialTransaction(spendTXBuilder)
|
||||
val spendTX = services.addSignature(spendPTX, freshKey)
|
||||
services.recordTransactions(spendTX)
|
||||
|
||||
}
|
||||
database.transaction {
|
||||
val consumedStates = vaultQuery.queryBy<ContractState>(VaultQueryCriteria(status = Vault.StateStatus.CONSUMED)).states
|
||||
assertEquals(3, consumedStates.count())
|
||||
|
||||
@ -300,17 +320,21 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `consuming multiple contract state types in same transaction`() {
|
||||
fun `consuming multiple contract state types`() {
|
||||
|
||||
val freshKey = services.keyManagementService.freshKey()
|
||||
val freshIdentity = AnonymousParty(freshKey)
|
||||
database.transaction {
|
||||
|
||||
services.fillWithSomeTestDeals(listOf("123", "456", "789"))
|
||||
val deals = vaultQuery.queryBy<DummyDealContract.State>().states
|
||||
deals.forEach { println(it.state.data.linearId.externalId!!) }
|
||||
|
||||
}
|
||||
val deals =
|
||||
database.transaction {
|
||||
vaultQuery.queryBy<DummyDealContract.State>().states
|
||||
}
|
||||
database.transaction {
|
||||
services.fillWithSomeTestLinearStates(3)
|
||||
}
|
||||
database.transaction {
|
||||
val linearStates = vaultQuery.queryBy<DummyLinearContract.State>().states
|
||||
linearStates.forEach { println(it.state.data.linearId) }
|
||||
|
||||
@ -324,10 +348,10 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
|
||||
}
|
||||
|
||||
val dummyMove = notaryServices.signInitialTransaction(dummyMoveBuilder)
|
||||
|
||||
dummyMove.toLedgerTransaction(services).verify()
|
||||
services.recordTransactions(dummyMove)
|
||||
|
||||
}
|
||||
database.transaction {
|
||||
val consumedStates = vaultQuery.queryBy<ContractState>(VaultQueryCriteria(status = Vault.StateStatus.CONSUMED)).states
|
||||
assertEquals(2, consumedStates.count())
|
||||
|
||||
|
Reference in New Issue
Block a user