From 369214a7473e5743750ae292575965d03500f369 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Wed, 19 Oct 2016 15:06:52 +0100 Subject: [PATCH] Additional method on VaultService to add notes to a transaction Additional method on VaultService to retrieve notes for a transaction --- .../r3corda/core/node/services/Services.kt | 11 +++- .../core/testing/InMemoryVaultService.kt | 0 .../node/services/vault/NodeVaultService.kt | 54 ++++++++++++++++--- .../node/services/NodeVaultServiceTest.kt | 54 +++++++++++++++++++ 4 files changed, 112 insertions(+), 7 deletions(-) create mode 100644 core/src/main/kotlin/com/r3corda/core/testing/InMemoryVaultService.kt diff --git a/core/src/main/kotlin/com/r3corda/core/node/services/Services.kt b/core/src/main/kotlin/com/r3corda/core/node/services/Services.kt index 322517c859..c310b321cb 100644 --- a/core/src/main/kotlin/com/r3corda/core/node/services/Services.kt +++ b/core/src/main/kotlin/com/r3corda/core/node/services/Services.kt @@ -4,6 +4,7 @@ import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.SettableFuture import com.r3corda.core.contracts.* import com.r3corda.core.crypto.Party +import com.r3corda.core.crypto.SecureHash import com.r3corda.core.transactions.TransactionBuilder import com.r3corda.core.transactions.WireTransaction import rx.Observable @@ -35,7 +36,8 @@ val DEFAULT_SESSION_ID = 0L * 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: Iterable>) { +class Vault(val states: Iterable>, + val transactionNotes: Map> = emptyMap()) { @Suppress("UNCHECKED_CAST") inline fun statesOfType() = states.filter { it.state.data is T } as List> @@ -164,6 +166,13 @@ interface VaultService { return future } + /** + * Add a note to an existing [LedgerTransaction] given by its unique [SecureHash] id + */ + fun addNoteToTransaction(txnId: SecureHash, noteText: String) + + fun getTransactionNotes(txnId: SecureHash): Iterable + /** * Fungible Asset operations **/ diff --git a/core/src/main/kotlin/com/r3corda/core/testing/InMemoryVaultService.kt b/core/src/main/kotlin/com/r3corda/core/testing/InMemoryVaultService.kt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/node/src/main/kotlin/com/r3corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/com/r3corda/node/services/vault/NodeVaultService.kt index 2e50747e49..9e0a727e65 100644 --- a/node/src/main/kotlin/com/r3corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/com/r3corda/node/services/vault/NodeVaultService.kt @@ -5,6 +5,7 @@ import com.r3corda.contracts.asset.Cash import com.r3corda.core.ThreadBox import com.r3corda.core.bufferUntilSubscribed import com.r3corda.core.contracts.* +import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.Party import com.r3corda.core.node.ServiceHub import com.r3corda.core.node.services.Vault @@ -14,10 +15,8 @@ import com.r3corda.core.transactions.TransactionBuilder import com.r3corda.core.transactions.WireTransaction import com.r3corda.core.utilities.loggerFor import com.r3corda.core.utilities.trace -import com.r3corda.node.utilities.AbstractJDBCHashSet -import com.r3corda.node.utilities.JDBCHashedTable -import com.r3corda.node.utilities.NODE_DATABASE_PREFIX -import com.r3corda.node.utilities.stateRef +import com.r3corda.node.utilities.* +import kotlinx.support.jdk8.collections.putIfAbsent import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.statements.InsertStatement import rx.Observable @@ -46,6 +45,11 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT val stateRef = stateRef("transaction_id", "output_index") } + private object TransactionNotesTable : JDBCHashedTable("${NODE_DATABASE_PREFIX}vault_txn_notes") { + val txnId = secureHash("txnId") + val notes = blob("notes") + } + private val mutex = ThreadBox(object { val unconsumedStates = object : AbstractJDBCHashSet(StatesSetTable) { override fun elementFromRow(row: ResultRow): StateRef = StateRef(row[table.stateRef.txId], row[table.stateRef.index]) @@ -56,6 +60,28 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT } } + val transactionNotes = object : AbstractJDBCHashMap, TransactionNotesTable>(TransactionNotesTable, loadOnInit = false) { + override fun keyFromRow(row: ResultRow): SecureHash { + return row[table.txnId] + } + + override fun valueFromRow(row: ResultRow): Set { + return deserializeFromBlob(row[table.notes]) + } + + override fun addKeyToInsert(insert: InsertStatement, entry: Map.Entry>, finalizables: MutableList<() -> Unit>) { + insert[table.txnId] = entry.key + } + + override fun addValueToInsert(insert: InsertStatement, entry: Map.Entry>, finalizables: MutableList<() -> Unit>) { + insert[table.notes] = serializeToBlob(entry.value, finalizables) + } + } + + fun allTransactionNotes(): Map> { + return transactionNotes + } + val _updatesPublisher = PublishSubject.create() fun allUnconsumedStates(): Iterable> { @@ -79,14 +105,14 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT } }) - override val currentVault: Vault get() = mutex.locked { Vault(allUnconsumedStates()) } + override val currentVault: Vault get() = mutex.locked { Vault(allUnconsumedStates(), allTransactionNotes()) } override val updates: Observable get() = mutex.locked { _updatesPublisher } override fun track(): Pair> { return mutex.locked { - Pair(Vault(allUnconsumedStates()), _updatesPublisher.bufferUntilSubscribed()) + Pair(Vault(allUnconsumedStates(), allTransactionNotes()), _updatesPublisher.bufferUntilSubscribed()) } } @@ -110,6 +136,22 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT return currentVault } + + override fun addNoteToTransaction(txnId: SecureHash, noteText: String) { + mutex.locked { + val notes = transactionNotes.getOrPut(key = txnId, defaultValue = { + setOf(noteText) + }) + transactionNotes.put(txnId, notes.plus(noteText)) + } + } + + override fun getTransactionNotes(txnId: SecureHash): Iterable { + mutex.locked { + return transactionNotes.get(txnId)!!.asIterable() + } + } + /** * Generate a transaction that moves an amount of currency to the given pubkey. * diff --git a/node/src/test/kotlin/com/r3corda/node/services/NodeVaultServiceTest.kt b/node/src/test/kotlin/com/r3corda/node/services/NodeVaultServiceTest.kt index ef314c7648..10405cba9c 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/NodeVaultServiceTest.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/NodeVaultServiceTest.kt @@ -1,15 +1,22 @@ package com.r3corda.node.services +import com.r3corda.contracts.asset.Cash import com.r3corda.contracts.testing.fillWithSomeTestCash import com.r3corda.core.contracts.DOLLARS +import com.r3corda.core.contracts.POUNDS +import com.r3corda.core.contracts.TransactionType +import com.r3corda.core.contracts.`issued by` import com.r3corda.core.node.services.TxWritableStorageService import com.r3corda.core.node.services.VaultService import com.r3corda.core.transactions.SignedTransaction import com.r3corda.core.utilities.DUMMY_NOTARY import com.r3corda.core.utilities.LogHelper +import com.r3corda.node.services.schema.NodeSchemaService import com.r3corda.node.services.vault.NodeVaultService import com.r3corda.node.utilities.configureDatabase import com.r3corda.node.utilities.databaseTransaction +import com.r3corda.testing.MEGA_CORP +import com.r3corda.testing.MEGA_CORP_KEY import com.r3corda.testing.node.MockServices import com.r3corda.testing.node.makeTestDataSourceProperties import org.assertj.core.api.Assertions.assertThat @@ -19,6 +26,7 @@ import org.junit.Before import org.junit.Test import java.io.Closeable import java.util.* +import kotlin.test.assertEquals class NodeVaultServiceTest { lateinit var dataSource: Closeable @@ -75,4 +83,50 @@ class NodeVaultServiceTest { assertThat(w2.states).hasSize(3) } } + + @Test + fun addNoteToTransaction() { + + databaseTransaction(database) { + val services = object : MockServices() { + override val vaultService: VaultService = NodeVaultService(this) + + override fun recordTransactions(txs: Iterable) { + for (stx in txs) { + storageService.validatedTransactions.addTransaction(stx) + } + // Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions. + vaultService.notifyAll(txs.map { it.tx }) + } + } + + val freshKey = services.legalIdentityKey + + // Issue a txn to Send us some Money + val usefulTX = TransactionType.General.Builder(null).apply { + Cash().generateIssue(this, 100.DOLLARS `issued by` MEGA_CORP.ref(1), freshKey.public, DUMMY_NOTARY) + signWith(MEGA_CORP_KEY) + }.toSignedTransaction() + + services.recordTransactions(listOf(usefulTX)) + + services.vaultService.addNoteToTransaction(usefulTX.id, "USD Sample Note 1") + services.vaultService.addNoteToTransaction(usefulTX.id, "USD Sample Note 2") + services.vaultService.addNoteToTransaction(usefulTX.id, "USD Sample Note 3") + assertEquals(1, services.vaultService.currentVault.transactionNotes.toList().size) + assertEquals(3, services.vaultService.getTransactionNotes(usefulTX.id).count()) + + // Issue more Money (GBP) + val anotherTX = TransactionType.General.Builder(null).apply { + Cash().generateIssue(this, 200.POUNDS `issued by` MEGA_CORP.ref(1), freshKey.public, DUMMY_NOTARY) + signWith(MEGA_CORP_KEY) + }.toSignedTransaction() + + services.recordTransactions(listOf(anotherTX)) + + services.vaultService.addNoteToTransaction(anotherTX.id, "GPB Sample Note 1") + assertEquals(2, services.vaultService.currentVault.transactionNotes.toList().size) + assertEquals(1, services.vaultService.getTransactionNotes(anotherTX.id).count()) + } + } } \ No newline at end of file