From 04920c9507f075114c363aebc34376c2735c6c08 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Wed, 19 Oct 2016 15:06:52 +0100 Subject: [PATCH 1/7] Additional method on VaultService to add notes to a transaction --- .../r3corda/core/node/services/Services.kt | 9 +++- .../core/testing/InMemoryVaultService.kt | 5 ++ .../node/services/vault/NodeVaultService.kt | 46 ++++++++++++++++--- .../node/services/NodeVaultServiceTest.kt | 39 ++++++++++++++++ 4 files changed, 92 insertions(+), 7 deletions(-) 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 f1939180dd..2c59054095 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.WireTransaction import rx.Observable import java.security.KeyPair @@ -33,7 +34,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> @@ -147,6 +149,11 @@ interface VaultService { } return future } + + /** + * Add a note to an existing [LedgerTransaction] given by its unique [SecureHash] id + */ + fun addNoteToTransaction(txnId: SecureHash, noteText: String) } inline fun VaultService.linearHeadsOfType() = linearHeadsOfType_(T::class.java) diff --git a/core/src/main/kotlin/com/r3corda/core/testing/InMemoryVaultService.kt b/core/src/main/kotlin/com/r3corda/core/testing/InMemoryVaultService.kt index f17c540f0d..19c5d81f37 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/InMemoryVaultService.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/InMemoryVaultService.kt @@ -3,6 +3,7 @@ package com.r3corda.core.testing 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.node.ServiceHub import com.r3corda.core.node.services.Vault import com.r3corda.core.node.services.VaultService @@ -94,6 +95,10 @@ open class InMemoryVaultService(protected val services: ServiceHub) : SingletonS return changedVault } + override fun addNoteToTransaction(txnId: SecureHash, noteText: String) { + throw UnsupportedOperationException("not implemented") //To change body of created functions use File | Settings | File Templates. + } + private fun isRelevant(state: ContractState, ourKeys: Set): Boolean { return if (state is OwnableState) { state.owner in ourKeys 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 995826cac6..42b22d20f5 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 @@ -4,6 +4,7 @@ import com.google.common.collect.Sets 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.node.ServiceHub import com.r3corda.core.node.services.Vault import com.r3corda.core.node.services.VaultService @@ -11,10 +12,7 @@ import com.r3corda.core.serialization.SingletonSerializeAsToken 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 org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.statements.InsertStatement import rx.Observable @@ -42,6 +40,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]) @@ -52,6 +55,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> { @@ -75,14 +100,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()) } } @@ -106,6 +131,15 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT return currentVault } + + override fun addNoteToTransaction(txnId: SecureHash, noteText: String) { + mutex.locked { + transactionNotes.getOrPut(key = txnId, defaultValue = { + setOf(noteText) + }).plus(noteText) + } + } + private fun makeUpdate(tx: WireTransaction, netDelta: Vault.Update, ourKeys: Set): Vault.Update { val ourNewStates = tx.outputs. filter { isRelevant(it.data, ourKeys) }. 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..bf01373dcb 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,21 @@ 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.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 +25,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 +82,36 @@ 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, "Sample Note 1") + assertEquals(1, services.vaultService.currentVault.transactionNotes.toList().size) + + } + } } \ No newline at end of file From 504ec42720ff8971127b526b64d50369a43b782a Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Thu, 27 Oct 2016 16:35:26 +0100 Subject: [PATCH 2/7] Additional method on VaultService to retrieve notes for a transaction --- .../com/r3corda/core/node/services/Services.kt | 2 ++ .../core/testing/InMemoryVaultService.kt | 4 ++++ .../node/services/vault/NodeVaultService.kt | 12 ++++++++++-- .../node/services/NodeVaultServiceTest.kt | 17 ++++++++++++++++- 4 files changed, 32 insertions(+), 3 deletions(-) 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 2c59054095..0426b88340 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 @@ -154,6 +154,8 @@ interface VaultService { * Add a note to an existing [LedgerTransaction] given by its unique [SecureHash] id */ fun addNoteToTransaction(txnId: SecureHash, noteText: String) + + fun getTransactionNotes(txnId: SecureHash): Iterable } inline fun VaultService.linearHeadsOfType() = linearHeadsOfType_(T::class.java) diff --git a/core/src/main/kotlin/com/r3corda/core/testing/InMemoryVaultService.kt b/core/src/main/kotlin/com/r3corda/core/testing/InMemoryVaultService.kt index 19c5d81f37..3813e99582 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/InMemoryVaultService.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/InMemoryVaultService.kt @@ -99,6 +99,10 @@ open class InMemoryVaultService(protected val services: ServiceHub) : SingletonS throw UnsupportedOperationException("not implemented") //To change body of created functions use File | Settings | File Templates. } + override fun getTransactionNotes(txnId: SecureHash): Iterable { + throw UnsupportedOperationException("not implemented") //To change body of created functions use File | Settings | File Templates. + } + private fun isRelevant(state: ContractState, ourKeys: Set): Boolean { return if (state is OwnableState) { state.owner in ourKeys 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 42b22d20f5..2810344ec5 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 @@ -13,6 +13,7 @@ import com.r3corda.core.transactions.WireTransaction import com.r3corda.core.utilities.loggerFor import com.r3corda.core.utilities.trace 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 @@ -134,9 +135,16 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT override fun addNoteToTransaction(txnId: SecureHash, noteText: String) { mutex.locked { - transactionNotes.getOrPut(key = txnId, defaultValue = { + val notes = transactionNotes.getOrPut(key = txnId, defaultValue = { setOf(noteText) - }).plus(noteText) + }) + transactionNotes.put(txnId, notes.plus(noteText)) + } + } + + override fun getTransactionNotes(txnId: SecureHash): Iterable { + mutex.locked { + return transactionNotes.get(txnId)!!.asIterable() } } 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 bf01373dcb..10405cba9c 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/NodeVaultServiceTest.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/NodeVaultServiceTest.kt @@ -3,6 +3,7 @@ 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 @@ -109,9 +110,23 @@ class NodeVaultServiceTest { services.recordTransactions(listOf(usefulTX)) - services.vaultService.addNoteToTransaction(usefulTX.id, "Sample Note 1") + 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 From 369214a7473e5743750ae292575965d03500f369 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Wed, 19 Oct 2016 15:06:52 +0100 Subject: [PATCH 3/7] 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 From 2ce310050e1a063b5e7e18cc9bc4adbb1b07cbc3 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Fri, 28 Oct 2016 10:09:47 +0100 Subject: [PATCH 4/7] Revert incorrect merge commit. --- .../r3corda/core/node/services/Services.kt | 1 - .../core/testing/InMemoryVaultService.kt | 142 ------------------ .../node/services/vault/NodeVaultService.kt | 2 - 3 files changed, 145 deletions(-) delete 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 6b1df4f765..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 @@ -6,7 +6,6 @@ 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.crypto.SecureHash import com.r3corda.core.transactions.WireTransaction import rx.Observable import java.security.KeyPair diff --git a/core/src/main/kotlin/com/r3corda/core/testing/InMemoryVaultService.kt b/core/src/main/kotlin/com/r3corda/core/testing/InMemoryVaultService.kt deleted file mode 100644 index 3813e99582..0000000000 --- a/core/src/main/kotlin/com/r3corda/core/testing/InMemoryVaultService.kt +++ /dev/null @@ -1,142 +0,0 @@ -package com.r3corda.core.testing - -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.node.ServiceHub -import com.r3corda.core.node.services.Vault -import com.r3corda.core.node.services.VaultService -import com.r3corda.core.serialization.SingletonSerializeAsToken -import com.r3corda.core.transactions.WireTransaction -import com.r3corda.core.utilities.loggerFor -import com.r3corda.core.utilities.trace -import rx.Observable -import rx.subjects.PublishSubject -import java.security.PublicKey -import java.util.* -import javax.annotation.concurrent.ThreadSafe - -/** - * This class implements a simple, in memory vault that tracks states that are owned by us, and also has a convenience - * method to auto-generate some self-issued cash states that can be used for test trading. A real vault would persist - * states relevant to us into a database and once such a vault is implemented, this scaffolding can be removed. - */ -@ThreadSafe -open class InMemoryVaultService(protected val services: ServiceHub) : SingletonSerializeAsToken(), VaultService { - open protected val log = loggerFor() - - // Variables inside InnerState are protected with a lock by the ThreadBox and aren't in scope unless you're - // inside mutex.locked {} code block. So we can't forget to take the lock unless we accidentally leak a reference - // to vault somewhere. - protected class InnerState { - var vault = Vault(emptyList>()) - val _updatesPublisher = PublishSubject.create() - } - - protected val mutex = ThreadBox(InnerState()) - - override val currentVault: Vault get() = mutex.locked { vault } - - override val updates: Observable - get() = mutex.content._updatesPublisher - - override fun track(): Pair> { - return mutex.locked { - Pair(vault, updates.bufferUntilSubscribed()) - } - } - - /** - * Returns a snapshot of the heads of LinearStates. - */ - override val linearHeads: Map> - get() = currentVault.let { vault -> - vault.states.filterStatesOfType().associateBy { it.state.data.linearId }.mapValues { it.value } - } - - override fun notifyAll(txns: Iterable): Vault { - val ourKeys = services.keyManagementService.keys.keys - - // Note how terribly incomplete this all is! - // - // - We don't notify anyone of anything, there are no event listeners. - // - We don't handle or even notice invalidations due to double spends of things in our vault. - // - We have no concept of confidence (for txns where there is no definite finality). - // - No notification that keys are used, for the case where we observe a spend of our own states. - // - No ability to create complex spends. - // - No logging or tracking of how the vault got into this state. - // - No persistence. - // - Does tx relevancy calculation and key management need to be interlocked? Probably yes. - // - // ... and many other things .... (Wallet.java in bitcoinj is several thousand lines long) - - var netDelta = Vault.NoUpdate - val changedVault = mutex.locked { - // Starting from the current vault, keep applying the transaction updates, calculating a new vault each - // time, until we get to the result (this is perhaps a bit inefficient, but it's functional and easily - // unit tested). - val vaultAndNetDelta = txns.fold(Pair(currentVault, Vault.NoUpdate)) { vaultAndDelta, tx -> - val (vault, delta) = vaultAndDelta.first.update(tx, ourKeys) - val combinedDelta = delta + vaultAndDelta.second - Pair(vault, combinedDelta) - } - - vault = vaultAndNetDelta.first - netDelta = vaultAndNetDelta.second - return@locked vault - } - - if (netDelta != Vault.NoUpdate) { - mutex.locked { - _updatesPublisher.onNext(netDelta) - } - } - return changedVault - } - - override fun addNoteToTransaction(txnId: SecureHash, noteText: String) { - throw UnsupportedOperationException("not implemented") //To change body of created functions use File | Settings | File Templates. - } - - override fun getTransactionNotes(txnId: SecureHash): Iterable { - throw UnsupportedOperationException("not implemented") //To change body of created functions use File | Settings | File Templates. - } - - private fun isRelevant(state: ContractState, ourKeys: Set): Boolean { - return if (state is OwnableState) { - state.owner in ourKeys - } else if (state is LinearState) { - // It's potentially of interest to the vault - state.isRelevant(ourKeys) - } else { - false - } - } - - private fun Vault.update(tx: WireTransaction, ourKeys: Set): Pair { - val ourNewStates = tx.outputs. - filter { isRelevant(it.data, ourKeys) }. - map { tx.outRef(it.data) } - - // Now calculate the states that are being spent by this transaction. - val consumed: Set = states.map { it.ref }.intersect(tx.inputs) - - // Is transaction irrelevant? - if (consumed.isEmpty() && ourNewStates.isEmpty()) { - log.trace { "tx ${tx.id} was irrelevant to this vault, ignoring" } - return Pair(this, Vault.NoUpdate) - } - - val change = Vault.Update(consumed, HashSet(ourNewStates)) - - // And calculate the new vault. - val newStates = states.filter { it.ref !in consumed } + ourNewStates - - log.trace { - "Applied tx ${tx.id.prefixChars()} to the vault: consumed ${consumed.size} states and added ${newStates.size}" - } - - return Pair(Vault(newStates), change) - } -} 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 16e497951f..6b181ae0a4 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,7 +5,6 @@ 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.crypto.SecureHash import com.r3corda.core.node.ServiceHub @@ -17,7 +16,6 @@ import com.r3corda.core.transactions.WireTransaction import com.r3corda.core.utilities.loggerFor import com.r3corda.core.utilities.trace 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 From 96007cd7775fefd8d1bfc3bbc8f0c65c4ce9af25 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Mon, 31 Oct 2016 15:07:34 +0000 Subject: [PATCH 5/7] Changed transaction notes type DB from 'blob' to 'text' --- .../com/r3corda/node/services/vault/NodeVaultService.kt | 8 ++++---- .../com/r3corda/node/services/NodeVaultServiceTest.kt | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) 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 6b181ae0a4..e51459d6ee 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 @@ -46,10 +46,10 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT private object TransactionNotesTable : JDBCHashedTable("${NODE_DATABASE_PREFIX}vault_txn_notes") { val txnId = secureHash("txnId") - val notes = blob("notes") + val notes = text("notes") } - private val mutex = ThreadBox(object { + private val mutex = ThreadBox(content = object { val unconsumedStates = object : AbstractJDBCHashSet(StatesSetTable) { override fun elementFromRow(row: ResultRow): StateRef = StateRef(row[table.stateRef.txId], row[table.stateRef.index]) @@ -65,7 +65,7 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT } override fun valueFromRow(row: ResultRow): Set { - return deserializeFromBlob(row[table.notes]) + return row[table.notes].split(delimiters = ";").toSet() } override fun addKeyToInsert(insert: InsertStatement, entry: Map.Entry>, finalizables: MutableList<() -> Unit>) { @@ -73,7 +73,7 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT } override fun addValueToInsert(insert: InsertStatement, entry: Map.Entry>, finalizables: MutableList<() -> Unit>) { - insert[table.notes] = serializeToBlob(entry.value, finalizables) + insert[table.notes] = entry.value.joinToString(separator = ";") } } 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 10405cba9c..1209c09bce 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/NodeVaultServiceTest.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/NodeVaultServiceTest.kt @@ -11,7 +11,6 @@ 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 From c5500caf98106e33b8cddcdfc0dfb8f1343c9141 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Tue, 1 Nov 2016 11:39:11 +0000 Subject: [PATCH 6/7] Minor changes to address PR feedback and comments --- .../kotlin/com/r3corda/core/node/services/Services.kt | 3 +++ .../r3corda/node/services/vault/NodeVaultService.kt | 10 ++-------- .../com/r3corda/node/services/NodeVaultServiceTest.kt | 1 - 3 files changed, 5 insertions(+), 9 deletions(-) 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 c310b321cb..e459f7269e 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 @@ -168,6 +168,9 @@ interface VaultService { /** * Add a note to an existing [LedgerTransaction] given by its unique [SecureHash] id + * Multiple notes may be attached to the same [LedgerTransaction]. + * These are additively and immutably persisted within the node local vault database in a single textual field + * using a semi-colon separator */ fun addNoteToTransaction(txnId: SecureHash, noteText: String) 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 e51459d6ee..9b0ca211dc 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 @@ -77,10 +77,6 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT } } - fun allTransactionNotes(): Map> { - return transactionNotes - } - val _updatesPublisher = PublishSubject.create() fun allUnconsumedStates(): Iterable> { @@ -104,14 +100,14 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT } }) - override val currentVault: Vault get() = mutex.locked { Vault(allUnconsumedStates(), allTransactionNotes()) } + override val currentVault: Vault get() = mutex.locked { Vault(allUnconsumedStates(), transactionNotes) } override val updates: Observable get() = mutex.locked { _updatesPublisher } override fun track(): Pair> { return mutex.locked { - Pair(Vault(allUnconsumedStates(), allTransactionNotes()), _updatesPublisher.bufferUntilSubscribed()) + Pair(Vault(allUnconsumedStates(), transactionNotes), _updatesPublisher.bufferUntilSubscribed()) } } @@ -135,7 +131,6 @@ 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 = { @@ -300,5 +295,4 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT false } } - } 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 1209c09bce..5d582522a5 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/NodeVaultServiceTest.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/NodeVaultServiceTest.kt @@ -85,7 +85,6 @@ class NodeVaultServiceTest { @Test fun addNoteToTransaction() { - databaseTransaction(database) { val services = object : MockServices() { override val vaultService: VaultService = NodeVaultService(this) From 74dc0b7154ffceb5e17a5e7482ec7b46d6df8abe Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Tue, 1 Nov 2016 12:46:41 +0000 Subject: [PATCH 7/7] Exposed Vault Transaction Note functionality via RPC. --- .../com/r3corda/node/internal/ServerRPCOps.kt | 13 +++++++++++++ .../r3corda/node/services/messaging/CordaRPCOps.kt | 11 +++++++++++ .../r3corda/node/services/ArtemisMessagingTests.kt | 8 ++++++++ 3 files changed, 32 insertions(+) diff --git a/node/src/main/kotlin/com/r3corda/node/internal/ServerRPCOps.kt b/node/src/main/kotlin/com/r3corda/node/internal/ServerRPCOps.kt index 38b1ec0a7f..bf2304a3ea 100644 --- a/node/src/main/kotlin/com/r3corda/node/internal/ServerRPCOps.kt +++ b/node/src/main/kotlin/com/r3corda/node/internal/ServerRPCOps.kt @@ -4,6 +4,7 @@ import com.r3corda.contracts.asset.Cash import com.r3corda.core.contracts.InsufficientBalanceException import com.r3corda.core.contracts.* import com.r3corda.core.crypto.Party +import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.toStringShort import com.r3corda.core.node.NodeInfo import com.r3corda.core.node.ServiceHub @@ -74,6 +75,18 @@ class ServerRPCOps( } } + override fun addVaultTransactionNote(txnId: SecureHash, txnNote: String) { + return databaseTransaction(database) { + services.vaultService.addNoteToTransaction(txnId, txnNote) + } + } + + override fun getVaultTransactionNotes(txnId: SecureHash): Iterable { + return databaseTransaction(database) { + services.vaultService.getTransactionNotes(txnId) + } + } + // TODO: Make a lightweight protocol that manages this workflow, rather than embedding it directly in the service private fun initiatePayment(req: ClientToServiceCommand.PayCash): TransactionBuildResult { val builder: TransactionBuilder = TransactionType.General.Builder(null) diff --git a/node/src/main/kotlin/com/r3corda/node/services/messaging/CordaRPCOps.kt b/node/src/main/kotlin/com/r3corda/node/services/messaging/CordaRPCOps.kt index 40060b0a07..0f7b616603 100644 --- a/node/src/main/kotlin/com/r3corda/node/services/messaging/CordaRPCOps.kt +++ b/node/src/main/kotlin/com/r3corda/node/services/messaging/CordaRPCOps.kt @@ -3,6 +3,7 @@ package com.r3corda.node.services.messaging import com.r3corda.core.contracts.ClientToServiceCommand import com.r3corda.core.contracts.ContractState import com.r3corda.core.contracts.StateAndRef +import com.r3corda.core.crypto.SecureHash import com.r3corda.core.node.NodeInfo import com.r3corda.core.node.services.NetworkMapCache import com.r3corda.core.node.services.StateMachineTransactionMapping @@ -116,4 +117,14 @@ interface CordaRPCOps : RPCOps { * TODO: The signature of this is weird because it's the remains of an old service call, we should have a call for each command instead. */ fun executeCommand(command: ClientToServiceCommand): TransactionBuildResult + + /* + * Add note(s) to an existing Vault transaction + */ + fun addVaultTransactionNote(txnId: SecureHash, txnNote: String) + + /* + * Retrieve existing note(s) for a given Vault transaction + */ + fun getVaultTransactionNotes(txnId: SecureHash): Iterable } \ No newline at end of file diff --git a/node/src/test/kotlin/com/r3corda/node/services/ArtemisMessagingTests.kt b/node/src/test/kotlin/com/r3corda/node/services/ArtemisMessagingTests.kt index 9d23a2b21e..5795c940b6 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/ArtemisMessagingTests.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/ArtemisMessagingTests.kt @@ -4,6 +4,7 @@ import com.google.common.net.HostAndPort import com.r3corda.core.contracts.ClientToServiceCommand import com.r3corda.core.contracts.ContractState import com.r3corda.core.contracts.StateAndRef +import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.generateKeyPair import com.r3corda.core.messaging.Message import com.r3corda.core.messaging.createMessage @@ -84,6 +85,13 @@ class ArtemisMessagingTests { throw UnsupportedOperationException("not implemented") //To change body of created functions use File | Settings | File Templates. } + override fun addVaultTransactionNote(txnId: SecureHash, txnNote: String) { + throw UnsupportedOperationException("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun getVaultTransactionNotes(txnId: SecureHash): Iterable { + throw UnsupportedOperationException("not implemented") //To change body of created functions use File | Settings | File Templates. + } } @Before