mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
Initial implementation of Vault Persistence using Requery (#191)
* Initial prototyping with Requery as a persistence replacement for Exposed/Hibernate Applied changes following PR review by RP Updated timestamp naming (removed committedTimestamp) and StateStatus (removed AWAITING_CONSENSUS) after discussion with RP. Removed FungibleState and LinearState schemas (and associated tests) - awaiting Requery uni-directional relationship fix. Added Transaction propagation such that requery re-uses any existing transaction context. Made requery default logging configurable (disabled by default) Nullable fields are now truly nullable (in the Kotlin and DDL sense) Fix for SimmValuation integration test. Workarounds applied to resolve Requery issues when sharing Transactional context. Addressed PR review comments from MH. Further updates following re-review by RP/MH Further updates following additional PR review comments by RP Minor update following additional PR review comments by RP Optimised makeUpdate state processing code. Resolved conflicts after rebase. Additional Unit tests and bug fix for correct spending of multiple contract state types within a single transaction. Required interface change to states() API to take a setOf (ContractStateClassTypes) Minor code clean-up. Re-write NodeVaultService consumed state makeUpdate function using SQL. * Resolve conflict after rebase from master
This commit is contained in:
parent
111a2fac08
commit
fa33336d38
@ -10,7 +10,7 @@ buildscript {
|
||||
// Dependency versions. Can run 'gradle dependencyUpdates' to find new versions of things.
|
||||
//
|
||||
// TODO: Sort this alphabetically.
|
||||
ext.kotlin_version = '1.0.5-2'
|
||||
ext.kotlin_version = '1.0.6'
|
||||
ext.quasar_version = '0.7.6' // TODO: Upgrade to 0.7.7+ when Quasar bug 238 is resolved.
|
||||
ext.asm_version = '0.5.3'
|
||||
ext.artemis_version = '1.5.1'
|
||||
@ -29,6 +29,8 @@ buildscript {
|
||||
ext.jopt_simple_version = '5.0.2'
|
||||
ext.jansi_version = '1.14'
|
||||
ext.hibernate_version = '5.2.6.Final'
|
||||
ext.rxjava_version = '1.2.4'
|
||||
ext.requery_version = '1.1.1'
|
||||
ext.dokka_version = '0.9.13'
|
||||
|
||||
repositories {
|
||||
@ -43,6 +45,7 @@ buildscript {
|
||||
classpath "net.corda.plugins:quasar-utils:$gradle_plugins_version"
|
||||
classpath "net.corda.plugins:cordformation:$gradle_plugins_version"
|
||||
classpath 'com.github.ben-manes:gradle-versions-plugin:0.13.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlin_version"
|
||||
classpath "org.jetbrains.dokka:dokka-gradle-plugin:${dokka_version}"
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ sourceSets {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
testCompile "junit:junit:$junit_version"
|
||||
testCompile "commons-fileupload:commons-fileupload:1.3.2"
|
||||
|
||||
@ -63,7 +64,7 @@ dependencies {
|
||||
compile "com.google.guava:guava:$guava_version"
|
||||
|
||||
// RxJava: observable streams of events.
|
||||
compile "io.reactivex:rxjava:1.2.4"
|
||||
compile "io.reactivex:rxjava:$rxjava_version"
|
||||
|
||||
// Kryo: object graph serialization.
|
||||
compile "com.esotericsoftware:kryo:4.0.0"
|
||||
@ -88,4 +89,7 @@ dependencies {
|
||||
|
||||
// RS API: Response type and codes for ApiUtils.
|
||||
compile "javax.ws.rs:javax.ws.rs-api:2.0"
|
||||
|
||||
// Requery: SQL based query & persistence for Kotlin
|
||||
compile "io.requery:requery-kotlin:$requery_version"
|
||||
}
|
||||
|
@ -32,13 +32,11 @@ val DEFAULT_SESSION_ID = 0L
|
||||
*
|
||||
* This abstract class has no references to Cash contracts.
|
||||
*
|
||||
* [states] Holds the states that are *active* and *relevant*.
|
||||
* [states] Holds a [VaultService] queried subset of states that are *active* and *relevant*.
|
||||
* Active means they haven't been consumed yet (or we don't know about it).
|
||||
* Relevant means they contain at least one of our pubkeys.
|
||||
*/
|
||||
class Vault(val states: List<StateAndRef<ContractState>>) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
inline fun <reified T : ContractState> statesOfType() = states.filter { it.state.data is T } as List<StateAndRef<T>>
|
||||
class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
|
||||
|
||||
/**
|
||||
* Represents an update observed by the vault that will be notified to observers. Include the [StateRef]s of
|
||||
@ -81,6 +79,10 @@ class Vault(val states: List<StateAndRef<ContractState>>) {
|
||||
companion object {
|
||||
val NoUpdate = Update(emptySet(), emptySet())
|
||||
}
|
||||
|
||||
enum class StateStatus {
|
||||
UNCONSUMED, CONSUMED
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -92,11 +94,6 @@ class Vault(val states: List<StateAndRef<ContractState>>) {
|
||||
* Note that transactions we've seen are held by the storage service, not the vault.
|
||||
*/
|
||||
interface VaultService {
|
||||
/**
|
||||
* Returns a read-only snapshot of the vault at the time the call is made. Note that if you consume states or
|
||||
* keys in this vault, you must inform the vault service so it can update its internal state.
|
||||
*/
|
||||
val currentVault: Vault
|
||||
|
||||
/**
|
||||
* Prefer the use of [updates] unless you know why you want to use this instead.
|
||||
@ -125,25 +122,13 @@ interface VaultService {
|
||||
* Atomically get the current vault and a stream of updates. Note that the Observable buffers updates until the
|
||||
* first subscriber is registered so as to avoid racing with early updates.
|
||||
*/
|
||||
fun track(): Pair<Vault, Observable<Vault.Update>>
|
||||
fun track(): Pair<Vault<ContractState>, Observable<Vault.Update>>
|
||||
|
||||
/**
|
||||
* Returns a snapshot of the heads of LinearStates.
|
||||
* Return unconsumed [ContractState]s for a given set of [StateRef]s
|
||||
* TODO: revisit and generalize this exposed API function.
|
||||
*/
|
||||
val linearHeads: Map<UniqueIdentifier, StateAndRef<LinearState>>
|
||||
|
||||
// TODO: When KT-10399 is fixed, rename this and remove the inline version below.
|
||||
|
||||
/** Returns the [linearHeads] only when the type of the state would be considered an 'instanceof' the given type. */
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T : LinearState> linearHeadsOfType_(stateType: Class<T>): Map<UniqueIdentifier, StateAndRef<T>> {
|
||||
return linearHeads.filterValues { stateType.isInstance(it.state.data) }.mapValues { StateAndRef(it.value.state as TransactionState<T>, it.value.ref) }
|
||||
}
|
||||
|
||||
fun statesForRefs(refs: List<StateRef>): Map<StateRef, TransactionState<*>?> {
|
||||
val refsToStates = currentVault.states.associateBy { it.ref }
|
||||
return refs.associateBy({ it }) { refsToStates[it]?.state }
|
||||
}
|
||||
fun statesForRefs(refs: List<StateRef>): Map<StateRef, TransactionState<*>?>
|
||||
|
||||
/**
|
||||
* Possibly update the vault by marking as spent states that these transactions consume, and adding any relevant
|
||||
@ -213,9 +198,24 @@ interface VaultService {
|
||||
amount: Amount<Currency>,
|
||||
to: CompositeKey,
|
||||
onlyFromParties: Set<AnonymousParty>? = null): Pair<TransactionBuilder, List<CompositeKey>>
|
||||
|
||||
/**
|
||||
* Return [ContractState]s of a given [Contract] type and list of [Vault.StateStatus]
|
||||
*/
|
||||
fun <T : ContractState> states(clazzes: Set<Class<T>>, statuses: EnumSet<Vault.StateStatus>): List<StateAndRef<T>>
|
||||
}
|
||||
|
||||
inline fun <reified T : LinearState> VaultService.linearHeadsOfType() = linearHeadsOfType_(T::class.java)
|
||||
inline fun <reified T: ContractState> VaultService.unconsumedStates(): List<StateAndRef<T>> =
|
||||
states(setOf(T::class.java), EnumSet.of(Vault.StateStatus.UNCONSUMED))
|
||||
|
||||
inline fun <reified T: ContractState> VaultService.consumedStates(): List<StateAndRef<T>> =
|
||||
states(setOf(T::class.java), EnumSet.of(Vault.StateStatus.CONSUMED))
|
||||
|
||||
/** Returns the [linearState] heads only when the type of the state would be considered an 'instanceof' the given type. */
|
||||
inline fun <reified T : LinearState> VaultService.linearHeadsOfType() =
|
||||
states(setOf(T::class.java), EnumSet.of(Vault.StateStatus.UNCONSUMED))
|
||||
.associateBy { it.state.data.linearId }.mapValues { it.value }
|
||||
|
||||
inline fun <reified T : DealState> VaultService.dealsWith(party: AnonymousParty) = linearHeadsOfType<T>().values.filter {
|
||||
it.state.data.parties.any { it == party }
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.corda.core.schemas
|
||||
|
||||
import io.requery.Persistable
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.serialization.toHexString
|
||||
@ -48,7 +49,7 @@ abstract class MappedSchema(schemaFamily: Class<*>,
|
||||
* A super class for all mapped states exported to a schema that ensures the [StateRef] appears on the database row. The
|
||||
* [StateRef] will be set to the correct value by the framework (there's no need to set during mapping generation by the state itself).
|
||||
*/
|
||||
@MappedSuperclass open class PersistentState(@EmbeddedId var stateRef: PersistentStateRef? = null)
|
||||
@MappedSuperclass open class PersistentState(@EmbeddedId var stateRef: PersistentStateRef? = null) : Persistable
|
||||
|
||||
/**
|
||||
* Embedded [StateRef] representation used in state mapping.
|
||||
@ -62,5 +63,9 @@ data class PersistentStateRef(
|
||||
var index: Int?
|
||||
) : Serializable {
|
||||
constructor(stateRef: StateRef) : this(stateRef.txhash.bytes.toHexString(), stateRef.index)
|
||||
/*
|
||||
JPA Query requirement:
|
||||
@Entity classes should have a default (non-arg) constructor to instantiate the objects when retrieving them from the database.
|
||||
*/
|
||||
constructor() : this(null, null)
|
||||
}
|
||||
|
@ -0,0 +1,26 @@
|
||||
package net.corda.core.schemas.requery
|
||||
|
||||
import io.requery.Key
|
||||
import io.requery.Persistable
|
||||
import io.requery.Superclass
|
||||
import net.corda.core.contracts.StateRef
|
||||
|
||||
import javax.persistence.Column
|
||||
|
||||
object Requery {
|
||||
/**
|
||||
* A super class for all mapped states exported to a schema that ensures the [StateRef] appears on the database row. The
|
||||
* [StateRef] will be set to the correct value by the framework (there's no need to set during mapping generation by the state itself).
|
||||
*/
|
||||
// TODO: this interface will supercede the existing [PersistentState] interface defined in PersistentTypes.kt
|
||||
// once we cut-over all existing Hibernate ContractState persistence to Requery
|
||||
@Superclass interface PersistentState : Persistable {
|
||||
@get:Key
|
||||
@get:Column(name = "transaction_id", length = 64)
|
||||
var txId: String
|
||||
|
||||
@get:Key
|
||||
@get:Column(name = "output_index")
|
||||
var index: Int
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package net.corda.core.schemas.requery.converters
|
||||
|
||||
import io.requery.Converter
|
||||
|
||||
import java.sql.*
|
||||
import java.time.*
|
||||
|
||||
/**
|
||||
* Converts from a [Instant] to a [java.sql.Timestamp] for Java 8. Note that
|
||||
* when converting between the time type and the database type all times will be converted to the
|
||||
* UTC zone offset.
|
||||
*/
|
||||
class InstantConverter : Converter<Instant, Timestamp> {
|
||||
|
||||
override fun getMappedType(): Class<Instant> { return Instant::class.java }
|
||||
|
||||
override fun getPersistedType(): Class<Timestamp> { return Timestamp::class.java }
|
||||
|
||||
override fun getPersistedSize(): Int? { return null }
|
||||
|
||||
override fun convertToPersisted(value: Instant?): Timestamp? {
|
||||
if (value == null) { return null }
|
||||
return Timestamp.from(value)
|
||||
}
|
||||
|
||||
override fun convertToMapped(type: Class<out Instant>, value: Timestamp?): Instant? {
|
||||
if (value == null) { return null }
|
||||
return value.toInstant()
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package net.corda.core.schemas.requery.converters
|
||||
|
||||
import io.requery.Converter
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.SecureHash
|
||||
|
||||
/**
|
||||
* Converts from a [StateRef] to a Composite Key defined by a [String] txnHash and an [Int] index
|
||||
*/
|
||||
class StateRefConverter : Converter<StateRef, Pair<String, Int>> {
|
||||
|
||||
override fun getMappedType(): Class<StateRef> { return StateRef::class.java }
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun getPersistedType(): Class<Pair<String,Int>> { return Pair::class.java as Class<Pair<String,Int>> }
|
||||
|
||||
override fun getPersistedSize(): Int? { return null }
|
||||
|
||||
override fun convertToPersisted(value: StateRef?): Pair<String,Int>? {
|
||||
if (value == null) { return null }
|
||||
return Pair(value.txhash.toString(), value.index)
|
||||
}
|
||||
|
||||
override fun convertToMapped(type: Class<out StateRef>, value: Pair<String,Int>?): StateRef? {
|
||||
if (value == null) { return null }
|
||||
return StateRef(SecureHash.parse(value.first), value.second)
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package net.corda.core.schemas.requery.converters
|
||||
|
||||
import io.requery.Converter
|
||||
import io.requery.converter.EnumOrdinalConverter
|
||||
import io.requery.sql.Mapping
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.node.services.Vault
|
||||
|
||||
import java.sql.*
|
||||
import java.time.*
|
||||
|
||||
/**
|
||||
* Converter which persists a [Vault.StateStatus] enum using its enum ordinal representation
|
||||
*/
|
||||
class VaultStateStatusConverter() : EnumOrdinalConverter<Vault.StateStatus>(Vault.StateStatus::class.java)
|
@ -7,6 +7,7 @@ import net.corda.core.crypto.Party
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.getOrThrow
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.node.services.unconsumedStates
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.utilities.Emoji
|
||||
import net.corda.flows.CashIssueFlow
|
||||
@ -159,7 +160,7 @@ class ContractUpgradeFlowTest {
|
||||
a.services.startFlow(ContractUpgradeFlow.Instigator(stateAndRef, CashV2::class.java))
|
||||
mockNet.runNetwork()
|
||||
// Get contract state form the vault.
|
||||
val state = databaseTransaction(a.database) { a.vault.currentVault.states }
|
||||
val state = databaseTransaction(a.database) { a.vault.unconsumedStates<ContractState>() }
|
||||
assertTrue(state.single().state.data is CashV2.State, "Contract state is upgraded to the new version.")
|
||||
assertEquals(Amount(1000000, USD).`issued by`(a.info.legalIdentity.ref(1)), (state.first().state.data as CashV2.State).amount, "Upgraded cash contain the correct amount.")
|
||||
assertEquals(listOf(a.info.legalIdentity.owningKey), (state.first().state.data as CashV2.State).owners, "Upgraded cash belongs to the right owner.")
|
||||
|
@ -0,0 +1,17 @@
|
||||
package net.corda.core.node.services
|
||||
|
||||
import org.assertj.core.api.Assertions
|
||||
import org.junit.Test
|
||||
|
||||
class VaultEnumTypesTest {
|
||||
@Test
|
||||
fun vaultStatusReflectsOrdinalValues() {
|
||||
/**
|
||||
* Warning!!! Do not change the order of this Enum as ordinal values are stored in the database
|
||||
*/
|
||||
val vaultStateStatusUnconsumed = Vault.StateStatus.UNCONSUMED
|
||||
Assertions.assertThat(vaultStateStatusUnconsumed.ordinal).isEqualTo(0)
|
||||
val vaultStateStatusConsumed = Vault.StateStatus.CONSUMED
|
||||
Assertions.assertThat(vaultStateStatusConsumed.ordinal).isEqualTo(1)
|
||||
}
|
||||
}
|
@ -13,6 +13,7 @@ import net.corda.core.crypto.signWithECDSA
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.node.PluginServiceHub
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.services.unconsumedStates
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.flows.FinalityFlow
|
||||
@ -42,7 +43,7 @@ private fun gatherOurInputs(serviceHub: ServiceHub,
|
||||
amountRequired: Amount<Issued<Currency>>,
|
||||
notary: Party?): Pair<List<StateAndRef<Cash.State>>, Long> {
|
||||
// Collect cash type inputs
|
||||
val cashStates = serviceHub.vaultService.currentVault.statesOfType<Cash.State>()
|
||||
val cashStates = serviceHub.vaultService.unconsumedStates<Cash.State>()
|
||||
// extract our key identity for convenience
|
||||
val ourKey = serviceHub.myInfo.legalIdentity.owningKey
|
||||
// Filter down to our own cash states with right currency and issuer
|
||||
|
@ -1,4 +1,7 @@
|
||||
apply plugin: 'kotlin'
|
||||
// Java Persistence API support: create no-arg constructor
|
||||
// see: http://stackoverflow.com/questions/32038177/kotlin-with-jpa-default-constructor-hell
|
||||
apply plugin: 'kotlin-jpa'
|
||||
apply plugin: CanonicalizerPlugin
|
||||
apply plugin: 'net.corda.plugins.publish-utils'
|
||||
apply plugin: 'net.corda.plugins.quasar-utils'
|
||||
|
@ -0,0 +1,31 @@
|
||||
package net.corda.contracts.testing
|
||||
|
||||
import net.corda.core.contracts.Contract
|
||||
import net.corda.core.contracts.DealState
|
||||
import net.corda.core.contracts.TransactionForContract
|
||||
import net.corda.core.contracts.UniqueIdentifier
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import java.security.PublicKey
|
||||
|
||||
class DummyDealContract: Contract {
|
||||
override val legalContractReference: SecureHash = SecureHash.sha256("TestDeal")
|
||||
|
||||
override fun verify(tx: TransactionForContract) {}
|
||||
|
||||
data class State(
|
||||
override val contract: Contract = DummyDealContract(),
|
||||
override val participants: List<CompositeKey> = listOf(),
|
||||
override val linearId: UniqueIdentifier = UniqueIdentifier(),
|
||||
override val ref: String,
|
||||
override val parties: List<Party> = listOf()) : DealState {
|
||||
override fun isRelevant(ourKeys: Set<PublicKey>): Boolean {
|
||||
return participants.any { it.containsAny(ourKeys) }
|
||||
}
|
||||
override fun generateAgreement(notary: Party): TransactionBuilder {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package net.corda.testing
|
||||
package net.corda.contracts.testing
|
||||
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.contracts.clauses.Clause
|
||||
@ -16,7 +16,7 @@ class DummyLinearContract: Contract {
|
||||
FilterOn(clause, { states -> states.filterIsInstance<State>() }),
|
||||
emptyList())
|
||||
|
||||
class State(
|
||||
data class State(
|
||||
override val linearId: UniqueIdentifier = UniqueIdentifier(),
|
||||
override val contract: Contract = DummyLinearContract(),
|
||||
override val participants: List<CompositeKey> = listOf(),
|
||||
@ -26,4 +26,4 @@ class DummyLinearContract: Contract {
|
||||
return participants.any { it.containsAny(ourKeys) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -11,14 +11,44 @@ import net.corda.core.contracts.PartyAndReference
|
||||
import net.corda.core.contracts.TransactionType
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.crypto.composite
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.recordTransactions
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.DUMMY_NOTARY
|
||||
import net.corda.core.utilities.DUMMY_NOTARY_KEY
|
||||
import java.security.KeyPair
|
||||
import java.util.*
|
||||
|
||||
fun ServiceHub.fillWithSomeTestDeals(dealIds: List<String>) {
|
||||
val freshKey = keyManagementService.freshKey()
|
||||
val transactions: List<SignedTransaction> = dealIds.map {
|
||||
// Issue a deal state
|
||||
val dummyIssue = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply {
|
||||
addOutputState(DummyDealContract.State(ref = it, participants = listOf(freshKey.public.composite)))
|
||||
signWith(freshKey)
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
}
|
||||
return@map dummyIssue.toSignedTransaction()
|
||||
}
|
||||
|
||||
recordTransactions(transactions)
|
||||
}
|
||||
|
||||
fun ServiceHub.fillWithSomeTestLinearStates(numberToCreate: Int) {
|
||||
val freshKey = keyManagementService.freshKey()
|
||||
for (i in 1..numberToCreate) {
|
||||
// Issue a deal state
|
||||
val dummyIssue = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply {
|
||||
addOutputState(DummyLinearContract.State(participants = listOf(freshKey.public.composite)))
|
||||
signWith(freshKey)
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
}
|
||||
recordTransactions(dummyIssue.toSignedTransaction())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a random set of between (by default) 3 and 10 cash states that add up to the given amount and adds them
|
||||
@ -38,7 +68,7 @@ fun ServiceHub.fillWithSomeTestCash(howMuch: Amount<Currency>,
|
||||
ref: OpaqueBytes = OpaqueBytes(ByteArray(1, { 1 })),
|
||||
ownedBy: CompositeKey? = null,
|
||||
issuedBy: PartyAndReference = DUMMY_CASH_ISSUER,
|
||||
issuerKey: KeyPair = DUMMY_CASH_ISSUER_KEY): Vault {
|
||||
issuerKey: KeyPair = DUMMY_CASH_ISSUER_KEY): Vault<Cash.State> {
|
||||
val amounts = calculateRandomlySizedAmounts(howMuch, atLeastThisManyStates, atMostThisManyStates, rng)
|
||||
|
||||
val myKey: CompositeKey = ownedBy ?: myInfo.legalIdentity.owningKey
|
||||
|
@ -4,6 +4,8 @@ import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.node.services.unconsumedStates
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
@ -34,7 +36,7 @@ class CashExitFlow(val amount: Amount<Currency>, val issueRef: OpaqueBytes, prog
|
||||
Cash().generateExit(
|
||||
builder,
|
||||
amount.issuedBy(issuer),
|
||||
serviceHub.vaultService.currentVault.statesOfType<Cash.State>().filter { it.state.data.owner == issuer.party.owningKey })
|
||||
serviceHub.vaultService.unconsumedStates<Cash.State>().filter { it.state.data.owner == issuer.party.owningKey })
|
||||
} catch (e: InsufficientBalanceException) {
|
||||
throw CashException("Exiting more cash than exists", e)
|
||||
}
|
||||
|
0
finance/src/main/kotlin/net/corda/flows/CashFlow.kt
Normal file
0
finance/src/main/kotlin/net/corda/flows/CashFlow.kt
Normal file
@ -2,9 +2,7 @@ package net.corda.schemas
|
||||
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.schemas.PersistentState
|
||||
import javax.persistence.Column
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.Table
|
||||
import javax.persistence.*
|
||||
|
||||
/**
|
||||
* An object used to fully qualify the [CashSchema] family name (i.e. independent of version).
|
||||
|
@ -1,7 +1,9 @@
|
||||
package net.corda.schemas
|
||||
|
||||
import io.requery.Convert
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.schemas.PersistentState
|
||||
import net.corda.core.schemas.requery.converters.InstantConverter
|
||||
import java.time.Instant
|
||||
import javax.persistence.Column
|
||||
import javax.persistence.Entity
|
||||
|
@ -210,24 +210,25 @@ class CommercialPaperTestsGeneric {
|
||||
*/
|
||||
|
||||
private lateinit var bigCorpServices: MockServices
|
||||
private lateinit var bigCorpVault: Vault
|
||||
private lateinit var bigCorpVault: Vault<ContractState>
|
||||
private lateinit var bigCorpVaultService: VaultService
|
||||
|
||||
private lateinit var aliceServices: MockServices
|
||||
private lateinit var aliceVaultService: VaultService
|
||||
private lateinit var alicesVault: Vault
|
||||
private lateinit var alicesVault: Vault<ContractState>
|
||||
|
||||
private lateinit var moveTX: SignedTransaction
|
||||
|
||||
@Test
|
||||
fun `issue move and then redeem`() {
|
||||
|
||||
val dataSourceAndDatabaseAlice = configureDatabase(makeTestDataSourceProperties())
|
||||
val dataSourcePropsAlice = makeTestDataSourceProperties()
|
||||
val dataSourceAndDatabaseAlice = configureDatabase(dataSourcePropsAlice)
|
||||
val databaseAlice = dataSourceAndDatabaseAlice.second
|
||||
databaseTransaction(databaseAlice) {
|
||||
|
||||
aliceServices = object : MockServices() {
|
||||
override val vaultService: VaultService = NodeVaultService(this)
|
||||
override val vaultService: VaultService = NodeVaultService(this, dataSourcePropsAlice)
|
||||
|
||||
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
|
||||
for (stx in txs) {
|
||||
@ -241,12 +242,13 @@ class CommercialPaperTestsGeneric {
|
||||
aliceVaultService = aliceServices.vaultService
|
||||
}
|
||||
|
||||
val dataSourceAndDatabaseBigCorp = configureDatabase(makeTestDataSourceProperties())
|
||||
val dataSourcePropsBigCorp = makeTestDataSourceProperties()
|
||||
val dataSourceAndDatabaseBigCorp = configureDatabase(dataSourcePropsBigCorp)
|
||||
val databaseBigCorp = dataSourceAndDatabaseBigCorp.second
|
||||
databaseTransaction(databaseBigCorp) {
|
||||
|
||||
bigCorpServices = object : MockServices() {
|
||||
override val vaultService: VaultService = NodeVaultService(this)
|
||||
override val vaultService: VaultService = NodeVaultService(this, dataSourcePropsBigCorp)
|
||||
|
||||
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
|
||||
for (stx in txs) {
|
||||
|
@ -3,8 +3,8 @@ package net.corda.contracts.asset
|
||||
import net.corda.contracts.testing.fillWithSomeTestCash
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.node.services.VaultService
|
||||
import net.corda.core.node.services.unconsumedStates
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
@ -46,18 +46,19 @@ class CashTests {
|
||||
val vault: VaultService get() = services.vaultService
|
||||
lateinit var dataSource: Closeable
|
||||
lateinit var database: Database
|
||||
lateinit var vaultService: Vault
|
||||
lateinit var vaultStatesUnconsumed: List<StateAndRef<Cash.State>>
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
LogHelper.setLevel(NodeVaultService::class)
|
||||
val dataSourceAndDatabase = configureDatabase(makeTestDataSourceProperties())
|
||||
val dataSourceProps = makeTestDataSourceProperties()
|
||||
val dataSourceAndDatabase = configureDatabase(dataSourceProps)
|
||||
dataSource = dataSourceAndDatabase.first
|
||||
database = dataSourceAndDatabase.second
|
||||
databaseTransaction(database) {
|
||||
services = object : MockServices() {
|
||||
override val keyManagementService: MockKeyManagementService = MockKeyManagementService(MINI_CORP_KEY, MEGA_CORP_KEY, OUR_KEY)
|
||||
override val vaultService: VaultService = NodeVaultService(this)
|
||||
override val vaultService: VaultService = NodeVaultService(this, dataSourceProps)
|
||||
|
||||
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
|
||||
for (stx in txs) {
|
||||
@ -77,7 +78,7 @@ class CashTests {
|
||||
services.fillWithSomeTestCash(howMuch = 80.SWISS_FRANCS, atLeastThisManyStates = 1, atMostThisManyStates = 1,
|
||||
issuedBy = MINI_CORP.ref(1), issuerKey = MINI_CORP_KEY, ownedBy = OUR_PUBKEY_1)
|
||||
|
||||
vaultService = services.vaultService.currentVault
|
||||
vaultStatesUnconsumed = services.vaultService.unconsumedStates<Cash.State>()
|
||||
}
|
||||
}
|
||||
|
||||
@ -566,7 +567,7 @@ class CashTests {
|
||||
val wtx = makeSpend(100.DOLLARS, THEIR_PUBKEY_1)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val vaultState = vaultService.states.elementAt(0) as StateAndRef<Cash.State>
|
||||
val vaultState = vaultStatesUnconsumed.elementAt(0)
|
||||
assertEquals(vaultState.ref, wtx.inputs[0])
|
||||
assertEquals(vaultState.state.data.copy(owner = THEIR_PUBKEY_1), wtx.outputs[0].data)
|
||||
assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
|
||||
@ -581,7 +582,7 @@ class CashTests {
|
||||
val tx = TransactionType.General.Builder(DUMMY_NOTARY)
|
||||
vault.generateSpend(tx, 80.DOLLARS, ALICE_PUBKEY, setOf(MINI_CORP))
|
||||
|
||||
assertEquals(vaultService.states.elementAt(2).ref, tx.inputStates()[0])
|
||||
assertEquals(vaultStatesUnconsumed.elementAt(2).ref, tx.inputStates()[0])
|
||||
}
|
||||
}
|
||||
|
||||
@ -593,7 +594,7 @@ class CashTests {
|
||||
val wtx = makeSpend(10.DOLLARS, THEIR_PUBKEY_1)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val vaultState = vaultService.states.elementAt(0) as StateAndRef<Cash.State>
|
||||
val vaultState = vaultStatesUnconsumed.elementAt(0)
|
||||
assertEquals(vaultState.ref, wtx.inputs[0])
|
||||
assertEquals(vaultState.state.data.copy(owner = THEIR_PUBKEY_1, amount = 10.DOLLARS `issued by` defaultIssuer), wtx.outputs[0].data)
|
||||
assertEquals(vaultState.state.data.copy(amount = 90.DOLLARS `issued by` defaultIssuer), wtx.outputs[1].data)
|
||||
@ -608,8 +609,8 @@ class CashTests {
|
||||
val wtx = makeSpend(500.DOLLARS, THEIR_PUBKEY_1)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val vaultState0 = vaultService.states.elementAt(0) as StateAndRef<Cash.State>
|
||||
val vaultState1 = vaultService.states.elementAt(1)
|
||||
val vaultState0 = vaultStatesUnconsumed.elementAt(0)
|
||||
val vaultState1 = vaultStatesUnconsumed.elementAt(1)
|
||||
assertEquals(vaultState0.ref, wtx.inputs[0])
|
||||
assertEquals(vaultState1.ref, wtx.inputs[1])
|
||||
assertEquals(vaultState0.state.data.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.outputs[0].data)
|
||||
@ -625,10 +626,10 @@ class CashTests {
|
||||
assertEquals(3, wtx.inputs.size)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val vaultState0 = vaultService.states.elementAt(0) as StateAndRef<Cash.State>
|
||||
val vaultState1 = vaultService.states.elementAt(1)
|
||||
val vaultState0 = vaultStatesUnconsumed.elementAt(0)
|
||||
val vaultState1 = vaultStatesUnconsumed.elementAt(1)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val vaultState2 = vaultService.states.elementAt(2) as StateAndRef<Cash.State>
|
||||
val vaultState2 = vaultStatesUnconsumed.elementAt(2)
|
||||
assertEquals(vaultState0.ref, wtx.inputs[0])
|
||||
assertEquals(vaultState1.ref, wtx.inputs[1])
|
||||
assertEquals(vaultState2.ref, wtx.inputs[2])
|
||||
|
34
node-schemas/build.gradle
Normal file
34
node-schemas/build.gradle
Normal file
@ -0,0 +1,34 @@
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
description 'Corda node database schemas'
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
maven {
|
||||
url 'http://oss.sonatype.org/content/repositories/snapshots'
|
||||
}
|
||||
jcenter()
|
||||
maven {
|
||||
url 'https://dl.bintray.com/kotlin/exposed'
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
kotlin {
|
||||
srcDir "${buildDir}/generated/source/kapt/main/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile project(':core')
|
||||
|
||||
// Requery: SQL based query & persistence for Kotlin
|
||||
kapt "io.requery:requery-processor:$requery_version"
|
||||
|
||||
// For H2 database support in persistence
|
||||
testCompile "com.h2database:h2:1.4.193"
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
package net.corda.node.services.vault.schemas
|
||||
|
||||
import io.requery.*
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.schemas.requery.Requery
|
||||
import net.corda.core.schemas.requery.converters.InstantConverter
|
||||
import java.time.Instant
|
||||
|
||||
object VaultSchema {
|
||||
|
||||
@Table(name = "vault_transaction_notes")
|
||||
@Entity(model = "vault")
|
||||
interface VaultTxnNote : Persistable {
|
||||
@get:Key
|
||||
@get:Generated
|
||||
@get:Column(name = "seq_no", index = true)
|
||||
var seqNo: Int
|
||||
|
||||
@get:Column(name = "transaction_id", length = 64, index = true)
|
||||
var txId: String
|
||||
|
||||
@get:Column(name = "note")
|
||||
var note: String
|
||||
}
|
||||
|
||||
@Table(name = "vault_cash_balances")
|
||||
@Entity(model = "vault")
|
||||
interface VaultCashBalances : Persistable {
|
||||
@get:Key
|
||||
@get:Column(name = "currency_code", length = 3)
|
||||
var currency: String
|
||||
|
||||
@get:Column(name = "amount", value = "0")
|
||||
var amount: Long
|
||||
}
|
||||
|
||||
@Table(name = "vault_states")
|
||||
@Entity(model = "vault")
|
||||
interface VaultStates : Requery.PersistentState {
|
||||
/** refers to the notary a state is attached to */
|
||||
@get:Column(name = "notary_name")
|
||||
var notaryName: String
|
||||
|
||||
@get:Column(name = "notary_key")
|
||||
var notaryKey: String
|
||||
|
||||
/** references a concrete ContractState that is [QueryableState] and has a [MappedSchema] */
|
||||
@get:Column(name = "contract_state_class_name")
|
||||
var contractStateClassName: String
|
||||
|
||||
/** refers to serialized transaction Contract State */
|
||||
// TODO: define contract state size maximum size and adjust length accordingly
|
||||
@get:Column(name = "contract_state", length = 10000)
|
||||
var contractState: ByteArray
|
||||
|
||||
/** state lifecycle: unconsumed, consumed */
|
||||
@get:Column(name = "state_status")
|
||||
var stateStatus: Vault.StateStatus
|
||||
|
||||
/** refers to timestamp recorded upon entering UNCONSUMED state */
|
||||
@get:Column(name = "recorded_timestamp")
|
||||
var recordedTime: Instant
|
||||
|
||||
/** refers to timestamp recorded upon entering CONSUMED state */
|
||||
@get:Column(name = "consumed_timestamp", nullable = true)
|
||||
var consumedTime: Instant?
|
||||
|
||||
/** used to denote a state has been soft locked (to prevent double spend)
|
||||
* will contain a temporary unique [UUID] obtained from a flow session */
|
||||
@get:Column(name = "lock_id", nullable = true)
|
||||
var lockId: String?
|
||||
|
||||
/** refers to the last time a lock was taken (reserved) or updated (released, re-reserved) */
|
||||
@get:Column(name = "lock_timestamp", nullable = true)
|
||||
var lockUpdateTime: Instant?
|
||||
}
|
||||
}
|
@ -0,0 +1,530 @@
|
||||
package net.corda.node.services.vault.schemas
|
||||
|
||||
import io.requery.Persistable
|
||||
import io.requery.TransactionIsolation
|
||||
import io.requery.kotlin.eq
|
||||
import io.requery.kotlin.invoke
|
||||
import io.requery.rx.KotlinRxEntityStore
|
||||
import io.requery.sql.*
|
||||
import io.requery.sql.platform.Generic
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.composite
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.schemas.requery.converters.InstantConverter
|
||||
import net.corda.core.schemas.requery.converters.VaultStateStatusConverter
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.utilities.DUMMY_NOTARY
|
||||
import net.corda.core.utilities.DUMMY_NOTARY_KEY
|
||||
import net.corda.core.utilities.DUMMY_PUBKEY_1
|
||||
import net.corda.core.utilities.DUMMY_PUBKEY_2
|
||||
import org.h2.jdbcx.JdbcDataSource
|
||||
import org.junit.After
|
||||
import org.junit.Assert
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import rx.Observable
|
||||
import java.sql.Connection
|
||||
import java.sql.DriverManager
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class VaultSchemaTest {
|
||||
|
||||
var instance : KotlinEntityDataStore<Persistable>? = null
|
||||
val data : KotlinEntityDataStore<Persistable> get() = instance!!
|
||||
|
||||
var oinstance : KotlinRxEntityStore<Persistable>? = null
|
||||
val odata : KotlinRxEntityStore<Persistable> get() = oinstance!!
|
||||
|
||||
var transaction : LedgerTransaction? = null
|
||||
|
||||
var jdbcInstance : Connection? = null
|
||||
val jdbcConn : Connection get() = jdbcInstance!!
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
val dataSource = JdbcDataSource()
|
||||
dataSource.setURL("jdbc:h2:mem:vault_persistence;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1")
|
||||
val configuration = KotlinConfiguration(dataSource = dataSource, model = Models.VAULT, mapping = setupCustomMapping())
|
||||
instance = KotlinEntityDataStore<Persistable>(configuration)
|
||||
oinstance = KotlinRxEntityStore(KotlinEntityDataStore<Persistable>(configuration))
|
||||
val tables = SchemaModifier(configuration)
|
||||
val mode = TableCreationMode.DROP_CREATE
|
||||
tables.createTables(mode)
|
||||
|
||||
jdbcInstance = DriverManager.getConnection(dataSource.getURL())
|
||||
|
||||
// create dummy test data
|
||||
setupDummyData()
|
||||
}
|
||||
|
||||
private fun setupCustomMapping(): Mapping? {
|
||||
val mapping = GenericMapping(Generic())
|
||||
val instantConverter = InstantConverter()
|
||||
mapping.addConverter(instantConverter, instantConverter.mappedType)
|
||||
val vaultStateStatusConverter = VaultStateStatusConverter()
|
||||
mapping.addConverter(vaultStateStatusConverter, vaultStateStatusConverter.mappedType)
|
||||
return mapping
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
data.close()
|
||||
}
|
||||
|
||||
private class VaultNoopContract() : Contract {
|
||||
override val legalContractReference = SecureHash.sha256("")
|
||||
data class VaultNoopState(override val owner: CompositeKey) : OwnableState {
|
||||
override val contract = VaultNoopContract()
|
||||
override val participants: List<CompositeKey>
|
||||
get() = listOf(owner)
|
||||
override fun withNewOwner(newOwner: CompositeKey) = Pair(Commands.Create(), copy(owner = newOwner))
|
||||
}
|
||||
interface Commands : CommandData {
|
||||
class Create : TypeOnlyCommandData(), Commands
|
||||
}
|
||||
|
||||
override fun verify(tx: TransactionForContract) {
|
||||
// Always accepts.
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupDummyData() {
|
||||
// dummy Transaction
|
||||
val notary: Party = DUMMY_NOTARY
|
||||
val inState1 = TransactionState(DummyContract.SingleOwnerState(0, DUMMY_PUBKEY_1), notary)
|
||||
val inState2 = TransactionState(DummyContract.MultiOwnerState(0,
|
||||
listOf(DUMMY_PUBKEY_1, DUMMY_PUBKEY_2)), notary)
|
||||
val inState3 = TransactionState(VaultNoopContract.VaultNoopState(DUMMY_PUBKEY_1), notary)
|
||||
val outState1 = inState1.copy()
|
||||
val outState2 = inState2.copy()
|
||||
val outState3 = inState3.copy()
|
||||
val inputs = listOf(StateAndRef(inState1, StateRef(SecureHash.randomSHA256(), 0)),
|
||||
StateAndRef(inState2, StateRef(SecureHash.randomSHA256(), 0)),
|
||||
StateAndRef(inState3, StateRef(SecureHash.randomSHA256(), 0)))
|
||||
val outputs = listOf(outState1, outState2, outState3)
|
||||
val commands = emptyList<AuthenticatedObject<CommandData>>()
|
||||
val attachments = emptyList<Attachment>()
|
||||
val id = SecureHash.randomSHA256()
|
||||
val signers = listOf(DUMMY_NOTARY_KEY.public.composite)
|
||||
val timestamp: Timestamp? = null
|
||||
transaction = LedgerTransaction(
|
||||
inputs,
|
||||
outputs,
|
||||
commands,
|
||||
attachments,
|
||||
id,
|
||||
notary,
|
||||
signers,
|
||||
timestamp,
|
||||
TransactionType.General()
|
||||
)
|
||||
}
|
||||
|
||||
private fun createTxnWithTwoStateTypes(): LedgerTransaction {
|
||||
val notary: Party = DUMMY_NOTARY
|
||||
val inState1 = TransactionState(DummyContract.SingleOwnerState(0, DUMMY_PUBKEY_1), notary)
|
||||
val inState2 = TransactionState(DummyContract.MultiOwnerState(0,
|
||||
listOf(DUMMY_PUBKEY_1, DUMMY_PUBKEY_2)), notary)
|
||||
val outState1 = inState1.copy()
|
||||
val outState2 = inState2.copy()
|
||||
val state1TxHash = SecureHash.randomSHA256()
|
||||
val state2TxHash = SecureHash.randomSHA256()
|
||||
val inputs = listOf(StateAndRef(inState1, StateRef(state1TxHash, 0)),
|
||||
StateAndRef(inState1, StateRef(state1TxHash, 1)),
|
||||
StateAndRef(inState2, StateRef(state2TxHash, 0)),
|
||||
StateAndRef(inState1, StateRef(state1TxHash, 2))) // bogus state not in db
|
||||
val outputs = listOf(outState1, outState2)
|
||||
val commands = emptyList<AuthenticatedObject<CommandData>>()
|
||||
val attachments = emptyList<Attachment>()
|
||||
val id = SecureHash.randomSHA256()
|
||||
val signers = listOf(DUMMY_NOTARY_KEY.public.composite)
|
||||
val timestamp: Timestamp? = null
|
||||
return LedgerTransaction(
|
||||
inputs,
|
||||
outputs,
|
||||
commands,
|
||||
attachments,
|
||||
id,
|
||||
notary,
|
||||
signers,
|
||||
timestamp,
|
||||
TransactionType.General()
|
||||
)
|
||||
}
|
||||
|
||||
private fun dummyStatesInsert(txn: LedgerTransaction) {
|
||||
data.invoke {
|
||||
// skip inserting the last txn state (to mimic spend attempt of non existent unconsumed state)
|
||||
txn.inputs.subList(0 , txn.inputs.lastIndex).forEach {
|
||||
insert(createStateEntity(it))
|
||||
// create additional state entities with idx >0
|
||||
for (i in 3..4) {
|
||||
try {
|
||||
createStateEntity(it, idx = i).apply {
|
||||
insert(this)
|
||||
}
|
||||
} catch(e: Exception) {}
|
||||
}
|
||||
// create additional state entities with different txn id
|
||||
for (i in 1..3) {
|
||||
createStateEntity(it, txHash = SecureHash.randomSHA256().toString()).apply {
|
||||
insert(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
// insert an additional MultiOwnerState with idx 1
|
||||
insert(createStateEntity(txn.inputs[2], idx = 1))
|
||||
|
||||
// insert entities with other state type
|
||||
for (i in 1..5) {
|
||||
VaultStatesEntity().apply {
|
||||
txId = SecureHash.randomSHA256().toString()
|
||||
index = 0
|
||||
contractStateClassName = VaultNoopContract.VaultNoopState::class.java.name
|
||||
stateStatus = Vault.StateStatus.UNCONSUMED
|
||||
insert(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check total numner of inserted states
|
||||
assertEquals(3+4+9+1+5, data.select(VaultSchema.VaultStates::class).get().count())
|
||||
}
|
||||
|
||||
/**
|
||||
* Vault Schema: VaultStates
|
||||
*/
|
||||
@Test
|
||||
fun testInsertState() {
|
||||
val state = VaultStatesEntity()
|
||||
state.txId = "12345"
|
||||
state.index = 0
|
||||
data.invoke {
|
||||
insert(state)
|
||||
val result = select(VaultSchema.VaultStates::class) where (VaultSchema.VaultStates::txId eq state.txId)
|
||||
Assert.assertSame(state, result().first())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUpsertUnconsumedState() {
|
||||
val stateEntity = createStateEntity(transaction!!.inputs[0])
|
||||
data.invoke {
|
||||
upsert(stateEntity)
|
||||
val result = select(VaultSchema.VaultStates::class) where (VaultSchema.VaultStates::txId eq stateEntity.txId)
|
||||
Assert.assertSame(stateEntity, result().first())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUpsertConsumedState() {
|
||||
val stateEntity = createStateEntity(transaction!!.inputs[0])
|
||||
data.invoke {
|
||||
upsert(stateEntity)
|
||||
}
|
||||
val keys = mapOf(VaultStatesEntity.TX_ID to stateEntity.txId,
|
||||
VaultStatesEntity.INDEX to stateEntity.index)
|
||||
val key = io.requery.proxy.CompositeKey(keys)
|
||||
data.invoke {
|
||||
val state = findByKey(VaultStatesEntity::class, key)
|
||||
state?.run {
|
||||
stateStatus = Vault.StateStatus.CONSUMED
|
||||
consumedTime = Instant.now()
|
||||
update(state)
|
||||
val result = select(VaultSchema.VaultStates::class) where (VaultSchema.VaultStates::txId eq state.txId)
|
||||
assertEquals(Vault.StateStatus.CONSUMED, result().first().stateStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCashBalanceUpdate() {
|
||||
val cashBalanceEntity = VaultCashBalancesEntity()
|
||||
cashBalanceEntity.currency = "USD"
|
||||
cashBalanceEntity.amount = 100
|
||||
data.invoke {
|
||||
val state = findByKey(VaultCashBalancesEntity::class, cashBalanceEntity.currency)
|
||||
assertNull(state)
|
||||
upsert(cashBalanceEntity)
|
||||
}
|
||||
data.invoke {
|
||||
val state = findByKey(VaultCashBalancesEntity::class, cashBalanceEntity.currency)
|
||||
state?.let {
|
||||
state.amount -= 80
|
||||
upsert(state)
|
||||
}
|
||||
assertEquals(20, state!!.amount)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTransactionalUpsertState() {
|
||||
data.withTransaction(TransactionIsolation.REPEATABLE_READ) {
|
||||
transaction!!.inputs.forEach {
|
||||
val stateEntity = createStateEntity(it)
|
||||
insert(stateEntity)
|
||||
}
|
||||
val result = select(VaultSchema.VaultStates::class)
|
||||
Assert.assertSame(3, result().toList().size)
|
||||
}
|
||||
data.invoke {
|
||||
val result = select(VaultSchema.VaultStates::class)
|
||||
Assert.assertSame(3, result().toList().size)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createStateEntity(stateAndRef: StateAndRef<*>, idx: Int? = null, txHash: String? = null): VaultStatesEntity {
|
||||
val stateRef = stateAndRef.ref
|
||||
val state = stateAndRef.state
|
||||
return VaultStatesEntity().apply {
|
||||
txId = txHash ?: stateRef.txhash.toString()
|
||||
index = idx ?: stateRef.index
|
||||
stateStatus = Vault.StateStatus.UNCONSUMED
|
||||
contractStateClassName = state.data.javaClass.name
|
||||
contractState = state.serialize().bytes
|
||||
notaryName = state.notary.name
|
||||
notaryKey = state.notary.owningKey.toBase58String()
|
||||
recordedTime = Instant.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vault Schema: Transaction Notes
|
||||
*/
|
||||
@Test
|
||||
fun testInsertTxnNote() {
|
||||
val txnNoteEntity = VaultTxnNoteEntity()
|
||||
txnNoteEntity.txId = "12345"
|
||||
txnNoteEntity.note = "Sample transaction note"
|
||||
data.invoke {
|
||||
insert(txnNoteEntity)
|
||||
val result = select(VaultSchema.VaultTxnNote::class)
|
||||
Assert.assertSame(txnNoteEntity, result().first())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFindTxnNote() {
|
||||
val txnNoteEntity = VaultTxnNoteEntity()
|
||||
txnNoteEntity.txId = "12345"
|
||||
txnNoteEntity.note = "Sample transaction note #1"
|
||||
val txnNoteEntity2 = VaultTxnNoteEntity()
|
||||
txnNoteEntity2.txId = "23456"
|
||||
txnNoteEntity2.note = "Sample transaction note #2"
|
||||
data.invoke {
|
||||
insert(txnNoteEntity)
|
||||
insert(txnNoteEntity2)
|
||||
}
|
||||
data.invoke {
|
||||
val result = select(VaultSchema.VaultTxnNote::class) where (VaultSchema.VaultTxnNote::txId eq txnNoteEntity2.txId)
|
||||
assertEquals(result().count(), 1)
|
||||
Assert.assertSame(txnNoteEntity2, result().first())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vault Schema: Cash Balances
|
||||
*/
|
||||
@Test
|
||||
fun testInsertCashBalance() {
|
||||
val cashBalanceEntity = VaultCashBalancesEntity()
|
||||
cashBalanceEntity.currency = "GPB"
|
||||
cashBalanceEntity.amount = 12345
|
||||
data.invoke {
|
||||
insert(cashBalanceEntity)
|
||||
val result = select(VaultSchema.VaultCashBalances::class)
|
||||
Assert.assertSame(cashBalanceEntity, result().first())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUpdateCashBalance() {
|
||||
val cashBalanceEntity = VaultCashBalancesEntity()
|
||||
cashBalanceEntity.currency = "GPB"
|
||||
cashBalanceEntity.amount = 12345
|
||||
data.invoke {
|
||||
insert(cashBalanceEntity)
|
||||
}
|
||||
data.invoke {
|
||||
val state = findByKey(VaultCashBalancesEntity::class, cashBalanceEntity.currency)
|
||||
assertNotNull(state)
|
||||
state?.let {
|
||||
state.amount += 10000
|
||||
update(state)
|
||||
val result = select(VaultCashBalancesEntity::class)
|
||||
assertEquals(22345, result().first().amount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUpsertCashBalance() {
|
||||
val cashBalanceEntity = VaultCashBalancesEntity()
|
||||
cashBalanceEntity.currency = "GPB"
|
||||
cashBalanceEntity.amount = 12345
|
||||
data.invoke {
|
||||
val state = findByKey(VaultCashBalancesEntity::class, cashBalanceEntity.currency)
|
||||
state?.let {
|
||||
state.amount += 10000
|
||||
}
|
||||
val result = upsert(state ?: cashBalanceEntity)
|
||||
assertEquals(12345, result.amount)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAllUnconsumedStates() {
|
||||
data.invoke {
|
||||
transaction!!.inputs.forEach {
|
||||
insert(createStateEntity(it))
|
||||
}
|
||||
}
|
||||
val stateAndRefs = unconsumedStates<ContractState>()
|
||||
assertNotNull(stateAndRefs)
|
||||
assertTrue { stateAndRefs.size == 3 }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun tesUnconsumedDummyStates() {
|
||||
data.invoke {
|
||||
transaction!!.inputs.forEach {
|
||||
insert(createStateEntity(it))
|
||||
}
|
||||
}
|
||||
val stateAndRefs = unconsumedStates<DummyContract.State>()
|
||||
assertNotNull(stateAndRefs)
|
||||
assertTrue { stateAndRefs.size == 2 }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun tesUnconsumedDummySingleOwnerStates() {
|
||||
data.invoke {
|
||||
transaction!!.inputs.forEach {
|
||||
insert(createStateEntity(it))
|
||||
}
|
||||
}
|
||||
val stateAndRefs = unconsumedStates<DummyContract.SingleOwnerState>()
|
||||
assertNotNull(stateAndRefs)
|
||||
assertTrue { stateAndRefs.size == 1 }
|
||||
}
|
||||
|
||||
inline fun <reified T: ContractState> unconsumedStates(): List<StateAndRef<T>> {
|
||||
val stateAndRefs =
|
||||
data.invoke {
|
||||
val result = select(VaultSchema.VaultStates::class)
|
||||
.where(VaultSchema.VaultStates::stateStatus eq Vault.StateStatus.UNCONSUMED)
|
||||
result.get()
|
||||
.map { it ->
|
||||
val stateRef = StateRef(SecureHash.parse(it.txId), it.index)
|
||||
val state = it.contractState.deserialize<TransactionState<T>>()
|
||||
StateAndRef(state, stateRef)
|
||||
}.filter {
|
||||
T::class.java.isAssignableFrom(it.state.data.javaClass)
|
||||
}.toList()
|
||||
}
|
||||
return stateAndRefs
|
||||
}
|
||||
|
||||
/**
|
||||
* Observables testing
|
||||
*/
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testInsert() {
|
||||
val stateEntity = createStateEntity(transaction!!.inputs[0])
|
||||
val latch = CountDownLatch(1)
|
||||
odata.insert(stateEntity).subscribe { stateEntity ->
|
||||
Assert.assertNotNull(stateEntity.txId)
|
||||
Assert.assertTrue(stateEntity.txId.isNotEmpty())
|
||||
val cached = data.select(VaultSchema.VaultStates::class)
|
||||
.where(VaultSchema.VaultStates::txId.eq(stateEntity.txId)).get().first()
|
||||
Assert.assertSame(cached, stateEntity)
|
||||
latch.countDown()
|
||||
}
|
||||
latch.await()
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testInsertCount() {
|
||||
val stateEntity = createStateEntity(transaction!!.inputs[0])
|
||||
Observable.just(stateEntity)
|
||||
.concatMap { person -> odata.insert(person).toObservable() }
|
||||
odata.insert(stateEntity).toBlocking().value()
|
||||
Assert.assertNotNull(stateEntity.txId)
|
||||
Assert.assertTrue(stateEntity.txId.isNotEmpty())
|
||||
val count = data.count(VaultSchema.VaultStates::class).get().value()
|
||||
Assert.assertEquals(1, count.toLong())
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testQueryEmpty() {
|
||||
val latch = CountDownLatch(1)
|
||||
odata.select(VaultSchema.VaultStates::class).get().toObservable()
|
||||
.subscribe({ Assert.fail() }, { Assert.fail() }) { latch.countDown() }
|
||||
if (!latch.await(1, TimeUnit.SECONDS)) {
|
||||
Assert.fail()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testQueryObservable() {
|
||||
transaction!!.inputs.forEach {
|
||||
val stateEntity = createStateEntity(it)
|
||||
odata.insert(stateEntity).toBlocking().value()
|
||||
}
|
||||
val states = ArrayList<VaultStatesEntity>()
|
||||
odata.select(VaultSchema.VaultStates::class).get()
|
||||
.toObservable()
|
||||
.subscribe { it -> states.add(it as VaultStatesEntity) }
|
||||
Assert.assertEquals(3, states.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testQueryWithCompositeKey() {
|
||||
// txn entity with 4 input states (SingleOwnerState x 3, MultiOwnerState x 1)
|
||||
val txn = createTxnWithTwoStateTypes()
|
||||
dummyStatesInsert(txn)
|
||||
|
||||
data.invoke {
|
||||
// Requery does not support SQL-92 select by composite key:
|
||||
// Raised Issue:
|
||||
// https://github.com/requery/requery/issues/434
|
||||
|
||||
// Test Requery raw query for single key field
|
||||
val refs = txn.inputs.map { it.ref }
|
||||
val objArgsTxHash = refs.map { it.txhash.toString() }
|
||||
val objArgsIndex = refs.map { it.index }
|
||||
|
||||
val queryByTxHashString = "SELECT * FROM VAULT_STATES WHERE transaction_id IN ?"
|
||||
val resultRawQueryTxHash = raw(VaultStatesEntity::class, queryByTxHashString, *objArgsTxHash.toTypedArray())
|
||||
assertEquals(8, resultRawQueryTxHash.count())
|
||||
|
||||
val queryByIndexString = "SELECT * FROM VAULT_STATES WHERE output_index IN ?"
|
||||
val resultRawQueryIndex = raw(VaultStatesEntity::class, queryByIndexString, *objArgsIndex.toTypedArray())
|
||||
assertEquals(18, resultRawQueryIndex.count())
|
||||
|
||||
// Use JDBC native query for composite key
|
||||
val stateRefs = refs.fold("") { stateRefs, it -> stateRefs + "('${it.txhash}','${it.index}')," }.dropLast(1)
|
||||
val statement = jdbcConn.createStatement()
|
||||
val rs = statement.executeQuery("SELECT transaction_id, output_index, contract_state FROM VAULT_STATES WHERE ((transaction_id, output_index) IN ($stateRefs)) AND (state_status = 0)")
|
||||
var count = 0
|
||||
while (rs.next()) count++
|
||||
assertEquals(3, count)
|
||||
}
|
||||
}
|
||||
}
|
@ -51,7 +51,10 @@ sourceSets {
|
||||
// build/reports/project/dependencies/index.html for green highlighted parts of the tree.
|
||||
|
||||
dependencies {
|
||||
|
||||
compile project(':finance')
|
||||
compile project(':node-schemas')
|
||||
|
||||
compile "com.google.code.findbugs:jsr305:3.0.1"
|
||||
|
||||
// Log4J: logging framework (with SLF4J bindings)
|
||||
|
@ -203,7 +203,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
||||
netMapCache = InMemoryNetworkMapCache()
|
||||
net = makeMessagingService()
|
||||
schemas = makeSchemaService()
|
||||
vault = makeVaultService()
|
||||
vault = makeVaultService(configuration.dataSourceProperties)
|
||||
|
||||
info = makeInfo()
|
||||
identity = makeIdentityService()
|
||||
@ -452,7 +452,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
||||
}
|
||||
|
||||
// TODO: sort out ordering of open & protected modifiers of functions in this class.
|
||||
protected open fun makeVaultService(): VaultService = NodeVaultService(services)
|
||||
protected open fun makeVaultService(dataSourceProperties: Properties): VaultService = NodeVaultService(services, dataSourceProperties)
|
||||
|
||||
protected open fun makeSchemaService(): SchemaService = NodeSchemaService()
|
||||
|
||||
|
@ -48,7 +48,7 @@ class CordaRPCOpsImpl(
|
||||
override fun vaultAndUpdates(): Pair<List<StateAndRef<ContractState>>, Observable<Vault.Update>> {
|
||||
return databaseTransaction(database) {
|
||||
val (vault, updates) = services.vaultService.track()
|
||||
Pair(vault.states, updates)
|
||||
Pair(vault.states.toList(), updates)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,149 @@
|
||||
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.StateRefConverter
|
||||
import net.corda.core.schemas.requery.converters.VaultStateStatusConverter
|
||||
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||
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)
|
||||
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 {
|
||||
val tx = TransactionManager.manager.currentOrNull()
|
||||
return CordaConnection(
|
||||
tx?.connection ?:
|
||||
TransactionManager.manager.newTransaction(Connection.TRANSACTION_REPEATABLE_READ).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
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
package net.corda.node.services.database
|
||||
|
||||
import com.zaxxer.hikari.HikariConfig
|
||||
import com.zaxxer.hikari.HikariDataSource
|
||||
import io.requery.Persistable
|
||||
import io.requery.meta.EntityModel
|
||||
import io.requery.sql.*
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||
import java.sql.Connection
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class RequeryConfiguration(val properties: Properties, val useDefaultLogging: Boolean = false) {
|
||||
|
||||
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)
|
||||
val mode = TableCreationMode.DROP_CREATE
|
||||
tables.createTables(mode)
|
||||
return KotlinEntityDataStore(configuration)
|
||||
}
|
||||
|
||||
// TODO: remove once Requery supports QUERY WITH COMPOSITE_KEY IN
|
||||
fun jdbcSession(): Connection {
|
||||
val ctx = TransactionManager.manager.currentOrNull()
|
||||
return ctx?.connection ?: TransactionManager.manager.newTransaction(Connection.TRANSACTION_REPEATABLE_READ).connection
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
package net.corda.node.services.vault
|
||||
|
||||
import com.google.common.collect.Sets
|
||||
import io.requery.TransactionIsolation
|
||||
import io.requery.kotlin.`in`
|
||||
import io.requery.kotlin.eq
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.core.ThreadBox
|
||||
import net.corda.core.bufferUntilSubscribed
|
||||
@ -11,16 +13,20 @@ import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.node.services.VaultService
|
||||
import net.corda.core.node.services.unconsumedStates
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.serialization.createKryo
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.tee
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.core.utilities.trace
|
||||
import net.corda.node.utilities.*
|
||||
import org.jetbrains.exposed.sql.ResultRow
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.statements.InsertStatement
|
||||
import net.corda.node.services.database.RequeryConfiguration
|
||||
import net.corda.node.services.vault.schemas.*
|
||||
import net.corda.node.utilities.bufferUntilDatabaseCommit
|
||||
import net.corda.node.utilities.wrapWithDatabaseTransaction
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
import java.security.PublicKey
|
||||
@ -37,66 +43,16 @@ import java.util.*
|
||||
* 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) : SingletonSerializeAsToken(), VaultService {
|
||||
class NodeVaultService(private val services: ServiceHub, dataSourceProperties: Properties) : SingletonSerializeAsToken(), VaultService {
|
||||
|
||||
private companion object {
|
||||
val log = loggerFor<NodeVaultService>()
|
||||
}
|
||||
|
||||
private object StatesSetTable : JDBCHashedTable("${NODE_DATABASE_PREFIX}vault_unconsumed_states") {
|
||||
val stateRef = stateRef("transaction_id", "output_index")
|
||||
}
|
||||
|
||||
private data class TxnNote(val txnId: SecureHash, val note: String) {
|
||||
override fun toString() = "$txnId: $note"
|
||||
}
|
||||
|
||||
private object CashBalanceTable : JDBCHashedTable("${NODE_DATABASE_PREFIX}vault_cash_balances") {
|
||||
val currency = varchar("currency", 3)
|
||||
val amount = long("amount")
|
||||
}
|
||||
|
||||
private object TransactionNotesTable : JDBCHashedTable("${NODE_DATABASE_PREFIX}vault_txn_notes") {
|
||||
val txnId = secureHash("txnId").index()
|
||||
val note = text("note")
|
||||
}
|
||||
val configuration = RequeryConfiguration(dataSourceProperties)
|
||||
val session = configuration.sessionForModel(Models.VAULT)
|
||||
|
||||
private val mutex = ThreadBox(content = object {
|
||||
val unconsumedStates = object : AbstractJDBCHashSet<StateRef, StatesSetTable>(StatesSetTable) {
|
||||
override fun elementFromRow(row: ResultRow): StateRef = StateRef(row[table.stateRef.txId], row[table.stateRef.index])
|
||||
|
||||
override fun addElementToInsert(insert: InsertStatement, entry: StateRef, finalizables: MutableList<() -> Unit>) {
|
||||
insert[table.stateRef.txId] = entry.txhash
|
||||
insert[table.stateRef.index] = entry.index
|
||||
}
|
||||
}
|
||||
|
||||
val transactionNotes = object : AbstractJDBCHashSet<TxnNote, TransactionNotesTable>(TransactionNotesTable) {
|
||||
override fun elementFromRow(row: ResultRow): TxnNote = TxnNote(row[table.txnId], row[table.note])
|
||||
|
||||
override fun addElementToInsert(insert: InsertStatement, entry: TxnNote, finalizables: MutableList<() -> Unit>) {
|
||||
insert[table.txnId] = entry.txnId
|
||||
insert[table.note] = entry.note
|
||||
}
|
||||
|
||||
// TODO: caching (2nd tier db cache) and db results filtering (max records, date, other)
|
||||
fun select(txnId: SecureHash): Iterable<String> {
|
||||
return table.select { table.txnId.eq(txnId) }.map { row -> row[table.note] }.toSet().asIterable()
|
||||
}
|
||||
}
|
||||
|
||||
val cashBalances = object : AbstractJDBCHashMap<Currency, Amount<Currency>, CashBalanceTable>(CashBalanceTable) {
|
||||
override fun keyFromRow(row: ResultRow): Currency = Currency.getInstance(row[table.currency])
|
||||
override fun valueFromRow(row: ResultRow): Amount<Currency> = Amount(row[table.amount], keyFromRow(row))
|
||||
|
||||
override fun addKeyToInsert(insert: InsertStatement, entry: Map.Entry<Currency, Amount<Currency>>, finalizables: MutableList<() -> Unit>) {
|
||||
insert[table.currency] = entry.key.currencyCode
|
||||
}
|
||||
|
||||
override fun addValueToInsert(insert: InsertStatement, entry: Map.Entry<Currency, Amount<Currency>>, finalizables: MutableList<() -> Unit>) {
|
||||
insert[table.amount] = entry.value.quantity
|
||||
}
|
||||
}
|
||||
|
||||
val _updatesPublisher = PublishSubject.create<Vault.Update>()
|
||||
val _rawUpdatesPublisher = PublishSubject.create<Vault.Update>()
|
||||
@ -105,22 +61,39 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT
|
||||
// For use during publishing only.
|
||||
val updatesPublisher: rx.Observer<Vault.Update> get() = _updatesPublisher.bufferUntilDatabaseCommit().tee(_rawUpdatesPublisher)
|
||||
|
||||
fun allUnconsumedStates(): List<StateAndRef<ContractState>> {
|
||||
// Ideally we'd map this transform onto a sequence, but we can't have a lazy list here, since accessing it
|
||||
// from a flow might end up trying to serialize the captured context - vault internal state or db context.
|
||||
return unconsumedStates.map {
|
||||
val storedTx = services.storageService.validatedTransactions.getTransaction(it.txhash) ?: throw Error("Found transaction hash ${it.txhash} in unconsumed contract states that is not in transaction storage.")
|
||||
StateAndRef(storedTx.tx.outputs[it.index], it)
|
||||
}
|
||||
}
|
||||
|
||||
fun recordUpdate(update: Vault.Update): Vault.Update {
|
||||
if (update != Vault.NoUpdate) {
|
||||
val producedStateRefs = update.produced.map { it.ref }
|
||||
val producedStateRefsMap = update.produced.associateBy { it.ref }
|
||||
val consumedStateRefs = update.consumed.map { it.ref }
|
||||
log.trace { "Removing $consumedStateRefs consumed contract states and adding $producedStateRefs produced contract states to the database." }
|
||||
unconsumedStates.removeAll(consumedStateRefs)
|
||||
unconsumedStates.addAll(producedStateRefs)
|
||||
|
||||
session.withTransaction(TransactionIsolation.REPEATABLE_READ) {
|
||||
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
|
||||
// TODO: revisit Kryo bug when using THREAD_LOCAL_KYRO
|
||||
contractState = it.value.state.serialize(createKryo()).bytes
|
||||
notaryName = it.value.state.notary.name
|
||||
notaryKey = it.value.state.notary.owningKey.toBase58String()
|
||||
recordedTime = services.clock.instant()
|
||||
}
|
||||
insert(state)
|
||||
}
|
||||
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()
|
||||
update(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return update
|
||||
}
|
||||
@ -133,8 +106,18 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT
|
||||
(produced.keys + consumed.keys).map { currency ->
|
||||
val producedAmount = produced[currency] ?: Amount(0, currency)
|
||||
val consumedAmount = consumed[currency] ?: Amount(0, currency)
|
||||
val currentBalance = cashBalances[currency] ?: Amount(0, currency)
|
||||
cashBalances[currency] = currentBalance + producedAmount - consumedAmount
|
||||
|
||||
val cashBalanceEntity = VaultCashBalancesEntity()
|
||||
cashBalanceEntity.currency = currency.currencyCode
|
||||
cashBalanceEntity.amount = producedAmount.quantity - consumedAmount.quantity
|
||||
|
||||
session.withTransaction(TransactionIsolation.REPEATABLE_READ) {
|
||||
val state = findByKey(VaultCashBalancesEntity::class, currency.currencyCode)
|
||||
state?.run {
|
||||
amount += producedAmount.quantity - consumedAmount.quantity
|
||||
}
|
||||
upsert(state ?: cashBalanceEntity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -147,9 +130,15 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT
|
||||
}
|
||||
})
|
||||
|
||||
override val cashBalances: Map<Currency, Amount<Currency>> get() = mutex.locked { HashMap(cashBalances) }
|
||||
|
||||
override val currentVault: Vault get() = mutex.locked { Vault(allUnconsumedStates()) }
|
||||
override val cashBalances: Map<Currency, Amount<Currency>> get() {
|
||||
val cashBalancesByCurrency =
|
||||
session.withTransaction(TransactionIsolation.REPEATABLE_READ) {
|
||||
val balances = select(VaultSchema.VaultCashBalances::class)
|
||||
balances.get().toList()
|
||||
}
|
||||
return cashBalancesByCurrency.associateBy({ Currency.getInstance(it.currency) },
|
||||
{ Amount(it.amount, Currency.getInstance(it.currency)) })
|
||||
}
|
||||
|
||||
override val rawUpdates: Observable<Vault.Update>
|
||||
get() = mutex.locked { _rawUpdatesPublisher }
|
||||
@ -157,23 +146,55 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT
|
||||
override val updates: Observable<Vault.Update>
|
||||
get() = mutex.locked { _updatesInDbTx }
|
||||
|
||||
override fun track(): Pair<Vault, Observable<Vault.Update>> {
|
||||
override fun track(): Pair<Vault<ContractState>, Observable<Vault.Update>> {
|
||||
return mutex.locked {
|
||||
Pair(Vault(allUnconsumedStates()), _updatesPublisher.bufferUntilSubscribed().wrapWithDatabaseTransaction())
|
||||
Pair(Vault(unconsumedStates<ContractState>()), _updatesPublisher.bufferUntilSubscribed().wrapWithDatabaseTransaction())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a snapshot of the heads of LinearStates.
|
||||
*
|
||||
* TODO: Represent this using an actual JDBCHashMap or look at vault design further.
|
||||
*/
|
||||
override val linearHeads: Map<UniqueIdentifier, StateAndRef<LinearState>>
|
||||
get() = currentVault.states.filterStatesOfType<LinearState>().associateBy { it.state.data.linearId }.mapValues { it.value }
|
||||
override fun <T: ContractState> states(clazzes: Set<Class<T>>, statuses: EnumSet<Vault.StateStatus>): List<StateAndRef<T>> {
|
||||
val stateAndRefs =
|
||||
session.withTransaction(TransactionIsolation.REPEATABLE_READ) {
|
||||
var result = select(VaultSchema.VaultStates::class)
|
||||
.where(VaultSchema.VaultStates::stateStatus `in` statuses)
|
||||
// TODO: temporary fix to continue supporting track() function (until becomes Typed)
|
||||
if (!clazzes.map {it.name}.contains(ContractState::class.java.name))
|
||||
result.and (VaultSchema.VaultStates::contractStateClassName `in` (clazzes.map { it.name }))
|
||||
result.get()
|
||||
.map { it ->
|
||||
val stateRef = StateRef(SecureHash.parse(it.txId), it.index)
|
||||
// TODO: revisit Kryo bug when using THREAD_LOCAL_KYRO
|
||||
val state = it.contractState.deserialize<TransactionState<T>>(createKryo())
|
||||
StateAndRef(state, stateRef)
|
||||
}.toList()
|
||||
}
|
||||
return stateAndRefs
|
||||
}
|
||||
|
||||
override fun statesForRefs(refs: List<StateRef>): Map<StateRef, TransactionState<*>?> {
|
||||
val stateAndRefs =
|
||||
session.withTransaction(TransactionIsolation.REPEATABLE_READ) {
|
||||
var results: List<StateAndRef<*>> = emptyList()
|
||||
refs.forEach {
|
||||
val result = select(VaultSchema.VaultStates::class)
|
||||
.where(VaultSchema.VaultStates::stateStatus eq Vault.StateStatus.UNCONSUMED)
|
||||
.and(VaultSchema.VaultStates::txId eq it.txhash.toString())
|
||||
.and(VaultSchema.VaultStates::index eq it.index)
|
||||
result.get()?.each {
|
||||
val stateRef = StateRef(SecureHash.parse(it.txId), it.index)
|
||||
val state = it.contractState.deserialize<TransactionState<*>>()
|
||||
results += StateAndRef(state, stateRef)
|
||||
}
|
||||
}
|
||||
results
|
||||
}
|
||||
|
||||
return stateAndRefs.associateBy({ it.ref }, { it.state })
|
||||
}
|
||||
|
||||
override fun notifyAll(txns: Iterable<WireTransaction>) {
|
||||
val ourKeys = services.keyManagementService.keys.keys
|
||||
val netDelta = txns.fold(Vault.NoUpdate) { netDelta, txn -> netDelta + makeUpdate(txn, netDelta, ourKeys) }
|
||||
val netDelta = txns.fold(Vault.NoUpdate) { netDelta, txn -> netDelta + makeUpdate(txn, ourKeys) }
|
||||
if (netDelta != Vault.NoUpdate) {
|
||||
mutex.locked {
|
||||
recordUpdate(netDelta)
|
||||
@ -184,14 +205,17 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT
|
||||
}
|
||||
|
||||
override fun addNoteToTransaction(txnId: SecureHash, noteText: String) {
|
||||
mutex.locked {
|
||||
transactionNotes.add(TxnNote(txnId, noteText))
|
||||
session.withTransaction(TransactionIsolation.REPEATABLE_READ) {
|
||||
val txnNoteEntity = VaultTxnNoteEntity()
|
||||
txnNoteEntity.txId = txnId.toString()
|
||||
txnNoteEntity.note = noteText
|
||||
insert(txnNoteEntity)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getTransactionNotes(txnId: SecureHash): Iterable<String> {
|
||||
mutex.locked {
|
||||
return transactionNotes.select(txnId)
|
||||
return session.withTransaction(TransactionIsolation.REPEATABLE_READ) {
|
||||
(select(VaultSchema.VaultTxnNote::class) where (VaultSchema.VaultTxnNote::txId eq txnId.toString())).get().asIterable().map { it.note }
|
||||
}
|
||||
}
|
||||
|
||||
@ -219,7 +243,7 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT
|
||||
//
|
||||
// Finally, we add the states to the provided partial transaction.
|
||||
|
||||
val assetsStates = currentVault.statesOfType<Cash.State>()
|
||||
val assetsStates = unconsumedStates<Cash.State>()
|
||||
|
||||
val currency = amount.token
|
||||
var acceptableCoins = run {
|
||||
@ -308,32 +332,36 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT
|
||||
return Pair(gathered, gatheredAmount)
|
||||
}
|
||||
|
||||
private fun makeUpdate(tx: WireTransaction, netDelta: Vault.Update, ourKeys: Set<PublicKey>): Vault.Update {
|
||||
private fun makeUpdate(tx: WireTransaction, ourKeys: Set<PublicKey>): Vault.Update {
|
||||
val ourNewStates = tx.outputs.
|
||||
filter { isRelevant(it.data, ourKeys) }.
|
||||
map { tx.outRef<ContractState>(it.data) }
|
||||
|
||||
// Now calculate the states that are being spent by this transaction.
|
||||
val consumedRefs = tx.inputs.toHashSet()
|
||||
// We use Guava union here as it's lazy for contains() which is how retainAll() is implemented.
|
||||
// i.e. retainAll() iterates over consumed, checking contains() on the parameter. Sets.union() does not physically create
|
||||
// a new collection and instead contains() just checks the contains() of both parameters, and so we don't end up
|
||||
// iterating over all (a potentially very large) unconsumedStates at any point.
|
||||
mutex.locked {
|
||||
consumedRefs.retainAll(Sets.union(netDelta.produced, unconsumedStates))
|
||||
// Retrieve all unconsumed states for this transaction's inputs
|
||||
val consumedStates = HashSet<StateAndRef<ContractState>>()
|
||||
if (tx.inputs.isNotEmpty()) {
|
||||
val stateRefs = tx.inputs.fold("") { stateRefs, it -> stateRefs + "('${it.txhash}','${it.index}')," }.dropLast(1)
|
||||
// TODO: using native JDBC until requery supports SELECT WHERE COMPOSITE_KEY IN
|
||||
// https://github.com/requery/requery/issues/434
|
||||
val statement = configuration.jdbcSession().createStatement()
|
||||
val rs = statement.executeQuery("SELECT transaction_id, output_index, contract_state " +
|
||||
"FROM vault_states " +
|
||||
"WHERE ((transaction_id, output_index) IN ($stateRefs)) " +
|
||||
"AND (state_status = 0)")
|
||||
while (rs.next()) {
|
||||
val txHash = SecureHash.parse(rs.getString(1))
|
||||
val index = rs.getInt(2)
|
||||
val state = rs.getBytes(3).deserialize<TransactionState<ContractState>>(createKryo())
|
||||
consumedStates.add(StateAndRef(state, StateRef(txHash, index)))
|
||||
}
|
||||
}
|
||||
|
||||
// Is transaction irrelevant?
|
||||
if (consumedRefs.isEmpty() && ourNewStates.isEmpty()) {
|
||||
if (consumedStates.isEmpty() && ourNewStates.isEmpty()) {
|
||||
log.trace { "tx ${tx.id} was irrelevant to this vault, ignoring" }
|
||||
return Vault.NoUpdate
|
||||
}
|
||||
|
||||
val consumedStates = consumedRefs.map {
|
||||
val state = services.loadState(it)
|
||||
StateAndRef(state, it)
|
||||
}.toSet()
|
||||
|
||||
return Vault.Update(consumedStates, ourNewStates.toHashSet())
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ import net.corda.core.messaging.StateMachineUpdate
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.node.services.unconsumedStates
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.flows.CashIssueFlow
|
||||
@ -67,7 +68,7 @@ class CordaRPCOpsImplTest {
|
||||
|
||||
// Check the monitoring service wallet is empty
|
||||
databaseTransaction(aliceNode.database) {
|
||||
assertFalse(aliceNode.services.vaultService.currentVault.states.iterator().hasNext())
|
||||
assertFalse(aliceNode.services.vaultService.unconsumedStates<ContractState>().iterator().hasNext())
|
||||
}
|
||||
|
||||
// Tell the monitoring service node to issue some cash
|
||||
|
@ -488,7 +488,7 @@ class TwoPartyTradeFlowTests {
|
||||
withError: Boolean,
|
||||
owner: CompositeKey,
|
||||
issuer: AnonymousParty,
|
||||
notary: Party): Pair<Vault, List<WireTransaction>> {
|
||||
notary: Party): Pair<Vault<ContractState>, List<WireTransaction>> {
|
||||
val interimOwnerKey = MEGA_CORP_PUBKEY
|
||||
// Bob (Buyer) has some cash he got from the Bank of Elbonia, Alice (Seller) has some commercial paper she
|
||||
// wants to sell to Bob.
|
||||
@ -526,7 +526,7 @@ class TwoPartyTradeFlowTests {
|
||||
this.verifies()
|
||||
}
|
||||
|
||||
val vault = Vault(listOf("bob cash 1".outputStateAndRef(), "bob cash 2".outputStateAndRef()))
|
||||
val vault = Vault<ContractState>(listOf("bob cash 1".outputStateAndRef(), "bob cash 2".outputStateAndRef()))
|
||||
return Pair(vault, listOf(eb1, bc1, bc2))
|
||||
}
|
||||
|
||||
@ -535,7 +535,7 @@ class TwoPartyTradeFlowTests {
|
||||
owner: CompositeKey,
|
||||
amount: Amount<Issued<Currency>>,
|
||||
attachmentID: SecureHash?,
|
||||
notary: Party): Pair<Vault, List<WireTransaction>> {
|
||||
notary: Party): Pair<Vault<ContractState>, List<WireTransaction>> {
|
||||
val ap = transaction(transactionBuilder = TransactionBuilder(notary = notary)) {
|
||||
output("alice's paper", notary = notary) {
|
||||
CommercialPaper.State(MEGA_CORP.ref(1, 2, 3), owner, amount, TEST_TX_TIME + 7.days)
|
||||
@ -552,7 +552,7 @@ class TwoPartyTradeFlowTests {
|
||||
}
|
||||
}
|
||||
|
||||
val vault = Vault(listOf("alice's paper".outputStateAndRef()))
|
||||
val vault = Vault<ContractState>(listOf("alice's paper".outputStateAndRef()))
|
||||
return Pair(vault, listOf(ap))
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,6 @@ import net.corda.node.services.api.ServiceHubInternal
|
||||
import net.corda.node.services.persistence.DataVending
|
||||
import net.corda.node.services.schema.NodeSchemaService
|
||||
import net.corda.node.services.statemachine.StateMachineManager
|
||||
import net.corda.node.services.vault.NodeVaultService
|
||||
import net.corda.testing.MOCK_IDENTITY_SERVICE
|
||||
import net.corda.testing.node.MockNetworkMapCache
|
||||
import net.corda.testing.node.MockStorageService
|
||||
@ -24,9 +23,8 @@ import java.time.Clock
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
@Suppress("LeakingThis")
|
||||
open class MockServiceHubInternal(
|
||||
customVault: VaultService? = null,
|
||||
val customVault: VaultService? = null,
|
||||
val keyManagement: KeyManagementService? = null,
|
||||
val net: MessagingServiceInternal? = null,
|
||||
val identity: IdentityService? = MOCK_IDENTITY_SERVICE,
|
||||
@ -37,7 +35,8 @@ open class MockServiceHubInternal(
|
||||
val flowFactory: FlowLogicRefFactory? = FlowLogicRefFactory(),
|
||||
val schemas: SchemaService? = NodeSchemaService()
|
||||
) : ServiceHubInternal() {
|
||||
override val vaultService: VaultService = customVault ?: NodeVaultService(this)
|
||||
override val vaultService: VaultService
|
||||
get() = customVault ?: throw UnsupportedOperationException()
|
||||
override val keyManagementService: KeyManagementService
|
||||
get() = keyManagement ?: throw UnsupportedOperationException()
|
||||
override val identityService: IdentityService
|
||||
|
@ -9,8 +9,10 @@ import net.corda.core.flows.FlowLogicRef
|
||||
import net.corda.core.flows.FlowLogicRefFactory
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.recordTransactions
|
||||
import net.corda.core.node.services.VaultService
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.utilities.DUMMY_NOTARY
|
||||
import net.corda.node.services.vault.NodeVaultService
|
||||
import net.corda.node.services.events.NodeSchedulerService
|
||||
import net.corda.node.services.persistence.DBCheckpointStorage
|
||||
import net.corda.node.services.statemachine.StateMachineManager
|
||||
@ -74,7 +76,8 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() {
|
||||
countDown = CountDownLatch(1)
|
||||
smmHasRemovedAllFlows = CountDownLatch(1)
|
||||
calls = 0
|
||||
val dataSourceAndDatabase = configureDatabase(makeTestDataSourceProperties())
|
||||
val dataSourceProps = makeTestDataSourceProperties()
|
||||
val dataSourceAndDatabase = configureDatabase(dataSourceProps)
|
||||
dataSource = dataSourceAndDatabase.first
|
||||
database = dataSourceAndDatabase.second
|
||||
|
||||
@ -82,6 +85,7 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() {
|
||||
val kms = MockKeyManagementService(ALICE_KEY)
|
||||
val mockMessagingService = InMemoryMessagingNetwork(false).InMemoryMessaging(false, InMemoryMessagingNetwork.PeerHandle(0, "None"), AffinityExecutor.ServiceAffinityExecutor("test", 1), database)
|
||||
services = object : MockServiceHubInternal(overrideClock = testClock, keyManagement = kms, net = mockMessagingService), TestReference {
|
||||
override val vaultService: VaultService = NodeVaultService(this, dataSourceProps)
|
||||
override val testReference = this@NodeSchedulerServiceTest
|
||||
}
|
||||
scheduler = NodeSchedulerService(database, services, factory, schedulerGatedExecutor)
|
||||
|
@ -9,6 +9,7 @@ import net.corda.core.contracts.`issued by`
|
||||
import net.corda.core.crypto.composite
|
||||
import net.corda.core.node.services.TxWritableStorageService
|
||||
import net.corda.core.node.services.VaultService
|
||||
import net.corda.core.node.services.unconsumedStates
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.DUMMY_NOTARY
|
||||
import net.corda.core.utilities.LogHelper
|
||||
@ -31,11 +32,12 @@ import kotlin.test.assertEquals
|
||||
class NodeVaultServiceTest {
|
||||
lateinit var dataSource: Closeable
|
||||
lateinit var database: Database
|
||||
private val dataSourceProps = makeTestDataSourceProperties()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
LogHelper.setLevel(NodeVaultService::class)
|
||||
val dataSourceAndDatabase = configureDatabase(makeTestDataSourceProperties())
|
||||
val dataSourceAndDatabase = configureDatabase(dataSourceProps)
|
||||
dataSource = dataSourceAndDatabase.first
|
||||
database = dataSourceAndDatabase.second
|
||||
}
|
||||
@ -50,7 +52,7 @@ class NodeVaultServiceTest {
|
||||
fun `states not local to instance`() {
|
||||
databaseTransaction(database) {
|
||||
val services1 = object : MockServices() {
|
||||
override val vaultService: VaultService = NodeVaultService(this)
|
||||
override val vaultService: VaultService = NodeVaultService(this, dataSourceProps)
|
||||
|
||||
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
|
||||
for (stx in txs) {
|
||||
@ -61,12 +63,13 @@ class NodeVaultServiceTest {
|
||||
}
|
||||
services1.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L))
|
||||
|
||||
val w1 = services1.vaultService.currentVault
|
||||
assertThat(w1.states).hasSize(3)
|
||||
val w1 = services1.vaultService.unconsumedStates<Cash.State>()
|
||||
assertThat(w1).hasSize(3)
|
||||
|
||||
val originalStorage = services1.storageService
|
||||
val originalVault = services1.vaultService
|
||||
val services2 = object : MockServices() {
|
||||
override val vaultService: VaultService = NodeVaultService(this)
|
||||
override val vaultService: VaultService get() = originalVault
|
||||
|
||||
// We need to be able to find the same transactions as before, too.
|
||||
override val storageService: TxWritableStorageService get() = originalStorage
|
||||
@ -79,8 +82,32 @@ class NodeVaultServiceTest {
|
||||
}
|
||||
}
|
||||
|
||||
val w2 = services2.vaultService.currentVault
|
||||
assertThat(w2.states).hasSize(3)
|
||||
val w2 = services2.vaultService.unconsumedStates<Cash.State>()
|
||||
assertThat(w2).hasSize(3)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `states for refs`() {
|
||||
databaseTransaction(database) {
|
||||
val services1 = object : MockServices() {
|
||||
override val vaultService: VaultService = NodeVaultService(this, dataSourceProps)
|
||||
|
||||
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
|
||||
for (stx in txs) {
|
||||
storageService.validatedTransactions.addTransaction(stx)
|
||||
vaultService.notify(stx.tx)
|
||||
}
|
||||
}
|
||||
}
|
||||
services1.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L))
|
||||
|
||||
val w1 = services1.vaultService.unconsumedStates<Cash.State>()
|
||||
assertThat(w1).hasSize(3)
|
||||
|
||||
val stateRefs = listOf(w1[1].ref, w1[2].ref)
|
||||
val states = services1.vaultService.statesForRefs(stateRefs)
|
||||
assertThat(states).hasSize(2)
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,7 +115,7 @@ class NodeVaultServiceTest {
|
||||
fun addNoteToTransaction() {
|
||||
databaseTransaction(database) {
|
||||
val services = object : MockServices() {
|
||||
override val vaultService: VaultService = NodeVaultService(this)
|
||||
override val vaultService: VaultService = NodeVaultService(this, dataSourceProps)
|
||||
|
||||
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
|
||||
for (stx in txs) {
|
||||
|
@ -3,10 +3,14 @@ package net.corda.node.services
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.contracts.asset.DUMMY_CASH_ISSUER
|
||||
import net.corda.contracts.testing.fillWithSomeTestCash
|
||||
import net.corda.contracts.testing.fillWithSomeTestDeals
|
||||
import net.corda.contracts.testing.fillWithSomeTestLinearStates
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.composite
|
||||
import net.corda.core.node.recordTransactions
|
||||
import net.corda.core.node.services.VaultService
|
||||
import net.corda.core.node.services.consumedStates
|
||||
import net.corda.core.node.services.unconsumedStates
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.DUMMY_NOTARY
|
||||
import net.corda.core.utilities.DUMMY_NOTARY_KEY
|
||||
@ -14,7 +18,10 @@ import net.corda.core.utilities.LogHelper
|
||||
import net.corda.node.services.vault.NodeVaultService
|
||||
import net.corda.node.utilities.configureDatabase
|
||||
import net.corda.node.utilities.databaseTransaction
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.BOB_KEY
|
||||
import net.corda.testing.BOB_PUBKEY
|
||||
import net.corda.testing.MEGA_CORP
|
||||
import net.corda.testing.MEGA_CORP_KEY
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.node.makeTestDataSourceProperties
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
@ -37,13 +44,14 @@ class VaultWithCashTest {
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
LogHelper.setLevel(NodeVaultService::class)
|
||||
val dataSourceAndDatabase = configureDatabase(makeTestDataSourceProperties())
|
||||
LogHelper.setLevel(VaultWithCashTest::class)
|
||||
val dataSourceProps = makeTestDataSourceProperties()
|
||||
val dataSourceAndDatabase = configureDatabase(dataSourceProps)
|
||||
dataSource = dataSourceAndDatabase.first
|
||||
database = dataSourceAndDatabase.second
|
||||
databaseTransaction(database) {
|
||||
services = object : MockServices() {
|
||||
override val vaultService: VaultService = NodeVaultService(this)
|
||||
override val vaultService: VaultService = NodeVaultService(this, dataSourceProps)
|
||||
|
||||
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
|
||||
for (stx in txs) {
|
||||
@ -58,7 +66,7 @@ class VaultWithCashTest {
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
LogHelper.reset(NodeVaultService::class)
|
||||
LogHelper.reset(VaultWithCashTest::class)
|
||||
dataSource.close()
|
||||
}
|
||||
|
||||
@ -68,15 +76,15 @@ class VaultWithCashTest {
|
||||
// Fix the PRNG so that we get the same splits every time.
|
||||
services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L))
|
||||
|
||||
val w = vault.currentVault
|
||||
assertEquals(3, w.states.toList().size)
|
||||
val w = vault.unconsumedStates<Cash.State>()
|
||||
assertEquals(3, w.toList().size)
|
||||
|
||||
val state = w.states.toList()[0].state.data as Cash.State
|
||||
val state = w.toList()[0].state.data
|
||||
assertEquals(30.45.DOLLARS `issued by` DUMMY_CASH_ISSUER, state.amount)
|
||||
assertEquals(services.key.public.composite, state.owner)
|
||||
|
||||
assertEquals(34.70.DOLLARS `issued by` DUMMY_CASH_ISSUER, (w.states.toList()[2].state.data as Cash.State).amount)
|
||||
assertEquals(34.85.DOLLARS `issued by` DUMMY_CASH_ISSUER, (w.states.toList()[1].state.data as Cash.State).amount)
|
||||
assertEquals(34.70.DOLLARS `issued by` DUMMY_CASH_ISSUER, (w.toList()[2].state.data).amount)
|
||||
assertEquals(34.85.DOLLARS `issued by` DUMMY_CASH_ISSUER, (w.toList()[1].state.data).amount)
|
||||
}
|
||||
}
|
||||
|
||||
@ -119,7 +127,6 @@ class VaultWithCashTest {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `branching LinearStates fails to verify`() {
|
||||
databaseTransaction(database) {
|
||||
@ -128,8 +135,8 @@ class VaultWithCashTest {
|
||||
|
||||
// Issue a linear state
|
||||
val dummyIssue = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply {
|
||||
addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshKey.public.composite)))
|
||||
addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshKey.public.composite)))
|
||||
addOutputState(net.corda.contracts.testing.DummyLinearContract.State(linearId = linearId, participants = listOf(freshKey.public.composite)))
|
||||
addOutputState(net.corda.contracts.testing.DummyLinearContract.State(linearId = linearId, participants = listOf(freshKey.public.composite)))
|
||||
signWith(freshKey)
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
}.toSignedTransaction()
|
||||
@ -149,7 +156,7 @@ class VaultWithCashTest {
|
||||
|
||||
// Issue a linear state
|
||||
val dummyIssue = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply {
|
||||
addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshKey.public.composite)))
|
||||
addOutputState(net.corda.contracts.testing.DummyLinearContract.State(linearId = linearId, participants = listOf(freshKey.public.composite)))
|
||||
signWith(freshKey)
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
}.toSignedTransaction()
|
||||
@ -157,11 +164,11 @@ class VaultWithCashTest {
|
||||
dummyIssue.toLedgerTransaction(services).verify()
|
||||
|
||||
services.recordTransactions(dummyIssue)
|
||||
assertEquals(1, vault.currentVault.states.toList().size)
|
||||
assertEquals(1, vault.unconsumedStates<net.corda.contracts.testing.DummyLinearContract.State>().size)
|
||||
|
||||
// Move the same state
|
||||
val dummyMove = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply {
|
||||
addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshKey.public.composite)))
|
||||
addOutputState(net.corda.contracts.testing.DummyLinearContract.State(linearId = linearId, participants = listOf(freshKey.public.composite)))
|
||||
addInputState(dummyIssue.tx.outRef<LinearState>(0))
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
}.toSignedTransaction()
|
||||
@ -169,7 +176,74 @@ class VaultWithCashTest {
|
||||
dummyIssue.toLedgerTransaction(services).verify()
|
||||
|
||||
services.recordTransactions(dummyMove)
|
||||
assertEquals(1, vault.currentVault.states.toList().size)
|
||||
assertEquals(1, vault.unconsumedStates<net.corda.contracts.testing.DummyLinearContract.State>().size)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `spending cash in vault of mixed state types works`() {
|
||||
|
||||
val freshKey = services.keyManagementService.freshKey()
|
||||
databaseTransaction(database) {
|
||||
services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L), ownedBy = freshKey.public.composite)
|
||||
services.fillWithSomeTestCash(100.SWISS_FRANCS, DUMMY_NOTARY, 2, 2, Random(0L))
|
||||
services.fillWithSomeTestCash(100.POUNDS, DUMMY_NOTARY, 1, 1, Random(0L))
|
||||
val cash = vault.unconsumedStates<Cash.State>()
|
||||
cash.forEach { println(it.state.data.amount) }
|
||||
|
||||
services.fillWithSomeTestDeals(listOf("123","456","789"))
|
||||
val deals = vault.unconsumedStates<net.corda.contracts.testing.DummyDealContract.State>()
|
||||
deals.forEach { println(it.state.data.ref) }
|
||||
}
|
||||
|
||||
databaseTransaction(database) {
|
||||
// A tx that spends our money.
|
||||
val spendTX = TransactionType.General.Builder(DUMMY_NOTARY).apply {
|
||||
vault.generateSpend(this, 80.DOLLARS, BOB_PUBKEY)
|
||||
signWith(freshKey)
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
}.toSignedTransaction()
|
||||
services.recordTransactions(spendTX)
|
||||
|
||||
val consumedStates = vault.consumedStates<ContractState>()
|
||||
assertEquals(3, consumedStates.count())
|
||||
|
||||
val unconsumedStates = vault.unconsumedStates<ContractState>()
|
||||
assertEquals(7, unconsumedStates.count())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `consuming multiple contract state types in same transaction`() {
|
||||
|
||||
val freshKey = services.keyManagementService.freshKey()
|
||||
databaseTransaction(database) {
|
||||
|
||||
services.fillWithSomeTestDeals(listOf("123","456","789"))
|
||||
val deals = vault.unconsumedStates<net.corda.contracts.testing.DummyDealContract.State>()
|
||||
deals.forEach { println(it.state.data.ref) }
|
||||
|
||||
services.fillWithSomeTestLinearStates(3)
|
||||
val linearStates = vault.unconsumedStates<net.corda.contracts.testing.DummyLinearContract.State>()
|
||||
linearStates.forEach { println(it.state.data.linearId) }
|
||||
|
||||
// Create a txn consuming different contract types
|
||||
val dummyMove = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply {
|
||||
addOutputState(net.corda.contracts.testing.DummyLinearContract.State(participants = listOf(freshKey.public.composite)))
|
||||
addOutputState(net.corda.contracts.testing.DummyDealContract.State(ref = "999", participants = listOf(freshKey.public.composite)))
|
||||
addInputState(linearStates[0])
|
||||
addInputState(deals[0])
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
}.toSignedTransaction()
|
||||
|
||||
dummyMove.toLedgerTransaction(services).verify()
|
||||
services.recordTransactions(dummyMove)
|
||||
|
||||
val consumedStates = vault.consumedStates<ContractState>()
|
||||
assertEquals(2, consumedStates.count())
|
||||
|
||||
val unconsumedStates = vault.unconsumedStates<ContractState>()
|
||||
assertEquals(6, unconsumedStates.count())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,172 @@
|
||||
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.DummyContract
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TransactionType
|
||||
import net.corda.core.crypto.DigitalSignature
|
||||
import net.corda.core.crypto.NullPublicKey
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.serialization.createKryo
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.core.utilities.DUMMY_NOTARY
|
||||
import net.corda.core.utilities.DUMMY_PUBKEY_1
|
||||
import net.corda.node.services.persistence.DBTransactionStorage
|
||||
import net.corda.node.services.vault.schemas.Models
|
||||
import net.corda.node.services.vault.schemas.VaultCashBalancesEntity
|
||||
import net.corda.node.services.vault.schemas.VaultSchema
|
||||
import net.corda.node.services.vault.schemas.VaultStatesEntity
|
||||
import net.corda.node.utilities.configureDatabase
|
||||
import net.corda.node.utilities.databaseTransaction
|
||||
import net.corda.testing.node.makeTestDataSourceProperties
|
||||
import org.assertj.core.api.Assertions
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.io.Closeable
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
class RequeryConfigurationTest {
|
||||
|
||||
lateinit var dataSource: Closeable
|
||||
lateinit var database: Database
|
||||
lateinit var transactionStorage: DBTransactionStorage
|
||||
lateinit var requerySession: KotlinEntityDataStore<Persistable>
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
val dataSourceProperties = makeTestDataSourceProperties()
|
||||
val dataSourceAndDatabase = configureDatabase(dataSourceProperties)
|
||||
dataSource = dataSourceAndDatabase.first
|
||||
database = dataSourceAndDatabase.second
|
||||
newTransactionStorage()
|
||||
newRequeryStorage(dataSourceProperties)
|
||||
}
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
dataSource.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `transaction inserts in same DB transaction scope across two persistence engines`() {
|
||||
val txn = newTransaction()
|
||||
|
||||
databaseTransaction(database) {
|
||||
transactionStorage.addTransaction(txn)
|
||||
requerySession.withTransaction {
|
||||
insert(createVaultStateEntity(txn))
|
||||
}
|
||||
}
|
||||
|
||||
databaseTransaction(database) {
|
||||
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()
|
||||
|
||||
databaseTransaction(database) {
|
||||
transactionStorage.addTransaction(txn)
|
||||
requerySession.withTransaction {
|
||||
upsert(createCashBalance())
|
||||
select(VaultSchema.VaultCashBalances::class).get().first()
|
||||
insert(createVaultStateEntity(txn))
|
||||
}
|
||||
}
|
||||
|
||||
databaseTransaction(database) {
|
||||
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()
|
||||
|
||||
databaseTransaction(database) {
|
||||
transactionStorage.addTransaction(txn)
|
||||
requerySession.withTransaction {
|
||||
insert(createVaultStateEntity(txn))
|
||||
}
|
||||
rollback()
|
||||
}
|
||||
|
||||
databaseTransaction(database) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 = DUMMY_PUBKEY_1).serialize(createKryo()).bytes
|
||||
notaryName = txn.tx.notary!!.name
|
||||
notaryKey = txn.tx.notary!!.owningKey.toBase58String()
|
||||
recordedTime = Instant.now()
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
private fun createCashBalance(): VaultCashBalancesEntity {
|
||||
val cashBalanceEntity = VaultCashBalancesEntity()
|
||||
cashBalanceEntity.currency = "GBP"
|
||||
cashBalanceEntity.amount = 12345
|
||||
return cashBalanceEntity
|
||||
}
|
||||
|
||||
private fun newTransactionStorage() {
|
||||
databaseTransaction(database) {
|
||||
transactionStorage = DBTransactionStorage()
|
||||
}
|
||||
}
|
||||
|
||||
private fun newRequeryStorage(dataSourceProperties: Properties) {
|
||||
databaseTransaction(database) {
|
||||
val configuration = RequeryConfiguration(dataSourceProperties, true)
|
||||
requerySession = configuration.sessionForModel(Models.VAULT)
|
||||
}
|
||||
}
|
||||
|
||||
private fun newTransaction(): SignedTransaction {
|
||||
val wtx = WireTransaction(
|
||||
inputs = listOf(StateRef(SecureHash.randomSHA256(), 0)),
|
||||
attachments = emptyList(),
|
||||
outputs = emptyList(),
|
||||
commands = emptyList(),
|
||||
notary = DUMMY_NOTARY,
|
||||
signers = emptyList(),
|
||||
type = TransactionType.General(),
|
||||
timestamp = null
|
||||
)
|
||||
return SignedTransaction(wtx.serialized, listOf(DigitalSignature.WithKey(NullPublicKey, ByteArray(1))), wtx.id)
|
||||
}
|
||||
}
|
@ -92,6 +92,21 @@ class DBTransactionStorageTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `two transactions with rollback`() {
|
||||
val firstTransaction = newTransaction()
|
||||
val secondTransaction = newTransaction()
|
||||
databaseTransaction(database) {
|
||||
transactionStorage.addTransaction(firstTransaction)
|
||||
transactionStorage.addTransaction(secondTransaction)
|
||||
rollback()
|
||||
}
|
||||
|
||||
databaseTransaction(database) {
|
||||
assertThat(transactionStorage.transactions).isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `two transactions in same DB transaction scope`() {
|
||||
val firstTransaction = newTransaction()
|
||||
|
@ -8,6 +8,7 @@ import net.corda.core.contracts.TransactionType
|
||||
import net.corda.core.contracts.USD
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.node.services.unconsumedStates
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.DUMMY_NOTARY
|
||||
import net.corda.flows.BroadcastTransactionFlow.NotifyTxRequest
|
||||
@ -47,12 +48,12 @@ class DataVendingServiceTests {
|
||||
ptx.signWith(registerKey)
|
||||
val tx = ptx.toSignedTransaction()
|
||||
databaseTransaction(vaultServiceNode.database) {
|
||||
assertEquals(0, vaultServiceNode.services.vaultService.currentVault.states.toList().size)
|
||||
assertEquals(0, vaultServiceNode.services.vaultService.unconsumedStates<Cash.State>().size)
|
||||
|
||||
registerNode.sendNotifyTx(tx, vaultServiceNode)
|
||||
|
||||
// Check the transaction is in the receiving node
|
||||
val actual = vaultServiceNode.services.vaultService.currentVault.states.singleOrNull()
|
||||
val actual = vaultServiceNode.services.vaultService.unconsumedStates<Cash.State>().singleOrNull()
|
||||
val expected = tx.tx.outRef<Cash.State>(0)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
@ -78,12 +79,12 @@ class DataVendingServiceTests {
|
||||
ptx.signWith(registerKey)
|
||||
val tx = ptx.toSignedTransaction(false)
|
||||
databaseTransaction(vaultServiceNode.database) {
|
||||
assertEquals(0, vaultServiceNode.services.vaultService.currentVault.states.toList().size)
|
||||
assertEquals(0, vaultServiceNode.services.vaultService.unconsumedStates<Cash.State>().size)
|
||||
|
||||
registerNode.sendNotifyTx(tx, vaultServiceNode)
|
||||
|
||||
// Check the transaction is not in the receiving node
|
||||
assertEquals(0, vaultServiceNode.services.vaultService.currentVault.states.toList().size)
|
||||
assertEquals(0, vaultServiceNode.services.vaultService.unconsumedStates<Cash.State>().size)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ rootProject.name = 'corda-project'
|
||||
include 'finance'
|
||||
include 'finance:isolated'
|
||||
include 'core'
|
||||
include 'node-schemas'
|
||||
include 'node'
|
||||
include 'node:capsule'
|
||||
include 'client'
|
||||
@ -19,4 +20,5 @@ include 'samples:irs-demo'
|
||||
include 'samples:network-visualiser'
|
||||
include 'samples:simm-valuation-demo'
|
||||
include 'samples:raft-notary-demo'
|
||||
include 'samples:bank-of-corda-demo'
|
||||
include 'samples:bank-of-corda-demo'
|
||||
|
||||
|
@ -149,7 +149,7 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false,
|
||||
|
||||
override fun makeIdentityService() = MockIdentityService(mockNet.identities)
|
||||
|
||||
override fun makeVaultService(): VaultService = NodeVaultService(services)
|
||||
override fun makeVaultService(dataSourceProperties: Properties): VaultService = NodeVaultService(services, dataSourceProperties)
|
||||
|
||||
override fun makeKeyManagementService(): KeyManagementService {
|
||||
return E2ETestKeyManagementService(partyKeys + (overrideServices?.values ?: emptySet()))
|
||||
|
@ -1,6 +1,5 @@
|
||||
package net.corda.testing.node
|
||||
|
||||
import kotlinx.support.jdk8.collections.putIfAbsent
|
||||
import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.contracts.PartyAndReference
|
||||
import net.corda.core.crypto.*
|
||||
@ -60,7 +59,7 @@ open class MockServices(val key: KeyPair = generateKeyPair()) : ServiceHub {
|
||||
override val vaultService: VaultService get() = throw UnsupportedOperationException()
|
||||
override val networkService: MessagingService get() = throw UnsupportedOperationException()
|
||||
override val networkMapCache: NetworkMapCache get() = throw UnsupportedOperationException()
|
||||
override val clock: Clock get() = throw UnsupportedOperationException()
|
||||
override val clock: Clock get() = Clock.systemUTC()
|
||||
override val schedulerService: SchedulerService get() = throw UnsupportedOperationException()
|
||||
override val myInfo: NodeInfo get() = NodeInfo(object : SingleMessageRecipient {}, Party("MegaCorp", key.public.composite))
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user