Merged in colljos-vault-transaction-notes (pull request #419)

Vault transaction notes (COR-451)
This commit is contained in:
Jose Coll 2016-11-02 15:01:51 +00:00
commit 678b006438
6 changed files with 140 additions and 9 deletions

View File

@ -4,6 +4,7 @@ import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.SettableFuture import com.google.common.util.concurrent.SettableFuture
import com.r3corda.core.contracts.* import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.transactions.TransactionBuilder import com.r3corda.core.transactions.TransactionBuilder
import com.r3corda.core.transactions.WireTransaction import com.r3corda.core.transactions.WireTransaction
import rx.Observable 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). * 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. * Relevant means they contain at least one of our pubkeys.
*/ */
class Vault(val states: Iterable<StateAndRef<ContractState>>) { class Vault(val states: Iterable<StateAndRef<ContractState>>,
val transactionNotes: Map<SecureHash, Set<String>> = emptyMap()) {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
inline fun <reified T : ContractState> statesOfType() = states.filter { it.state.data is T } as List<StateAndRef<T>> inline fun <reified T : ContractState> statesOfType() = states.filter { it.state.data is T } as List<StateAndRef<T>>
@ -164,6 +166,16 @@ interface VaultService {
return future return future
} }
/**
* 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)
fun getTransactionNotes(txnId: SecureHash): Iterable<String>
/** /**
* [InsufficientBalanceException] is thrown when a Cash Spending transaction fails because * [InsufficientBalanceException] is thrown when a Cash Spending transaction fails because
* there is insufficient quantity for a given currency (and optionally set of Issuer Parties). * there is insufficient quantity for a given currency (and optionally set of Issuer Parties).

View File

@ -4,6 +4,7 @@ import com.r3corda.contracts.asset.Cash
import com.r3corda.core.contracts.InsufficientBalanceException import com.r3corda.core.contracts.InsufficientBalanceException
import com.r3corda.core.contracts.* import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.toStringShort import com.r3corda.core.crypto.toStringShort
import com.r3corda.core.node.NodeInfo import com.r3corda.core.node.NodeInfo
import com.r3corda.core.node.ServiceHub 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<String> {
return databaseTransaction(database) {
services.vaultService.getTransactionNotes(txnId)
}
}
// TODO: Make a lightweight protocol that manages this workflow, rather than embedding it directly in the service // TODO: Make a lightweight protocol that manages this workflow, rather than embedding it directly in the service
private fun initiatePayment(req: ClientToServiceCommand.PayCash): TransactionBuildResult { private fun initiatePayment(req: ClientToServiceCommand.PayCash): TransactionBuildResult {
val builder: TransactionBuilder = TransactionType.General.Builder(null) val builder: TransactionBuilder = TransactionType.General.Builder(null)

View File

@ -3,6 +3,7 @@ package com.r3corda.node.services.messaging
import com.r3corda.core.contracts.ClientToServiceCommand import com.r3corda.core.contracts.ClientToServiceCommand
import com.r3corda.core.contracts.ContractState import com.r3corda.core.contracts.ContractState
import com.r3corda.core.contracts.StateAndRef import com.r3corda.core.contracts.StateAndRef
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.node.NodeInfo import com.r3corda.core.node.NodeInfo
import com.r3corda.core.node.services.NetworkMapCache import com.r3corda.core.node.services.NetworkMapCache
import com.r3corda.core.node.services.StateMachineTransactionMapping 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. * 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 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<String>
} }

View File

@ -6,6 +6,7 @@ import com.r3corda.core.ThreadBox
import com.r3corda.core.bufferUntilSubscribed import com.r3corda.core.bufferUntilSubscribed
import com.r3corda.core.contracts.* import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.node.ServiceHub import com.r3corda.core.node.ServiceHub
import com.r3corda.core.node.services.Vault import com.r3corda.core.node.services.Vault
import com.r3corda.core.node.services.VaultService import com.r3corda.core.node.services.VaultService
@ -14,10 +15,7 @@ import com.r3corda.core.transactions.TransactionBuilder
import com.r3corda.core.transactions.WireTransaction import com.r3corda.core.transactions.WireTransaction
import com.r3corda.core.utilities.loggerFor import com.r3corda.core.utilities.loggerFor
import com.r3corda.core.utilities.trace import com.r3corda.core.utilities.trace
import com.r3corda.node.utilities.AbstractJDBCHashSet import com.r3corda.node.utilities.*
import com.r3corda.node.utilities.JDBCHashedTable
import com.r3corda.node.utilities.NODE_DATABASE_PREFIX
import com.r3corda.node.utilities.stateRef
import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.statements.InsertStatement import org.jetbrains.exposed.sql.statements.InsertStatement
import rx.Observable import rx.Observable
@ -46,7 +44,12 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT
val stateRef = stateRef("transaction_id", "output_index") val stateRef = stateRef("transaction_id", "output_index")
} }
private val mutex = ThreadBox(object { private object TransactionNotesTable : JDBCHashedTable("${NODE_DATABASE_PREFIX}vault_txn_notes") {
val txnId = secureHash("txnId")
val notes = text("notes")
}
private val mutex = ThreadBox(content = object {
val unconsumedStates = object : AbstractJDBCHashSet<StateRef, StatesSetTable>(StatesSetTable) { val unconsumedStates = object : AbstractJDBCHashSet<StateRef, StatesSetTable>(StatesSetTable) {
override fun elementFromRow(row: ResultRow): StateRef = StateRef(row[table.stateRef.txId], row[table.stateRef.index]) override fun elementFromRow(row: ResultRow): StateRef = StateRef(row[table.stateRef.txId], row[table.stateRef.index])
@ -56,6 +59,24 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT
} }
} }
val transactionNotes = object : AbstractJDBCHashMap<SecureHash, Set<String>, TransactionNotesTable>(TransactionNotesTable, loadOnInit = false) {
override fun keyFromRow(row: ResultRow): SecureHash {
return row[table.txnId]
}
override fun valueFromRow(row: ResultRow): Set<String> {
return row[table.notes].split(delimiters = ";").toSet()
}
override fun addKeyToInsert(insert: InsertStatement, entry: Map.Entry<SecureHash, Set<String>>, finalizables: MutableList<() -> Unit>) {
insert[table.txnId] = entry.key
}
override fun addValueToInsert(insert: InsertStatement, entry: Map.Entry<SecureHash, Set<String>>, finalizables: MutableList<() -> Unit>) {
insert[table.notes] = entry.value.joinToString(separator = ";")
}
}
val _updatesPublisher = PublishSubject.create<Vault.Update>() val _updatesPublisher = PublishSubject.create<Vault.Update>()
fun allUnconsumedStates(): Iterable<StateAndRef<ContractState>> { fun allUnconsumedStates(): Iterable<StateAndRef<ContractState>> {
@ -79,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(), transactionNotes) }
override val updates: Observable<Vault.Update> override val updates: Observable<Vault.Update>
get() = mutex.locked { _updatesPublisher } get() = mutex.locked { _updatesPublisher }
override fun track(): Pair<Vault, Observable<Vault.Update>> { override fun track(): Pair<Vault, Observable<Vault.Update>> {
return mutex.locked { return mutex.locked {
Pair(Vault(allUnconsumedStates()), _updatesPublisher.bufferUntilSubscribed()) Pair(Vault(allUnconsumedStates(), transactionNotes), _updatesPublisher.bufferUntilSubscribed())
} }
} }
@ -110,6 +131,21 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT
return currentVault 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<String> {
mutex.locked {
return transactionNotes.get(txnId)!!.asIterable()
}
}
/** /**
* Generate a transaction that moves an amount of currency to the given pubkey. * Generate a transaction that moves an amount of currency to the given pubkey.
* *
@ -264,5 +300,4 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT
false false
} }
} }
} }

View File

@ -4,6 +4,7 @@ import com.google.common.net.HostAndPort
import com.r3corda.core.contracts.ClientToServiceCommand import com.r3corda.core.contracts.ClientToServiceCommand
import com.r3corda.core.contracts.ContractState import com.r3corda.core.contracts.ContractState
import com.r3corda.core.contracts.StateAndRef import com.r3corda.core.contracts.StateAndRef
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.generateKeyPair import com.r3corda.core.crypto.generateKeyPair
import com.r3corda.core.messaging.Message import com.r3corda.core.messaging.Message
import com.r3corda.core.messaging.createMessage 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. 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<String> {
throw UnsupportedOperationException("not implemented") //To change body of created functions use File | Settings | File Templates.
}
} }
@Before @Before

View File

@ -1,7 +1,11 @@
package com.r3corda.node.services package com.r3corda.node.services
import com.r3corda.contracts.asset.Cash
import com.r3corda.contracts.testing.fillWithSomeTestCash import com.r3corda.contracts.testing.fillWithSomeTestCash
import com.r3corda.core.contracts.DOLLARS 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.TxWritableStorageService
import com.r3corda.core.node.services.VaultService import com.r3corda.core.node.services.VaultService
import com.r3corda.core.transactions.SignedTransaction import com.r3corda.core.transactions.SignedTransaction
@ -10,6 +14,8 @@ import com.r3corda.core.utilities.LogHelper
import com.r3corda.node.services.vault.NodeVaultService import com.r3corda.node.services.vault.NodeVaultService
import com.r3corda.node.utilities.configureDatabase import com.r3corda.node.utilities.configureDatabase
import com.r3corda.node.utilities.databaseTransaction 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.MockServices
import com.r3corda.testing.node.makeTestDataSourceProperties import com.r3corda.testing.node.makeTestDataSourceProperties
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
@ -19,6 +25,7 @@ import org.junit.Before
import org.junit.Test import org.junit.Test
import java.io.Closeable import java.io.Closeable
import java.util.* import java.util.*
import kotlin.test.assertEquals
class NodeVaultServiceTest { class NodeVaultServiceTest {
lateinit var dataSource: Closeable lateinit var dataSource: Closeable
@ -75,4 +82,49 @@ class NodeVaultServiceTest {
assertThat(w2.states).hasSize(3) 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<SignedTransaction>) {
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())
}
}
} }