Expose a JDBC connection (session) via the ServiceHub for generic JDB… (#1000)

* Expose a JDBC connection (session) via the ServiceHub for generic JDBC usage.

* Updated documentation.

* Fix failing JUnit following rebase from master.

* JDBC session Tutorial WIP

* Fix broken JUnits following update to consumeCash() using observable event update as return.

* Fixed broken JUnits.

* Refactoring createSession() into new CordaPersistence class following rebase from master.

* Updated fully working example.

* Minor updates following feedback from MH.

* Fixed compiler error following rebase.

* Fixes following rebase from master.

* Further updates and clarifications to documentation.
This commit is contained in:
josecoll
2017-08-04 09:26:27 +01:00
committed by GitHub
parent 818cbce789
commit 014387162d
12 changed files with 368 additions and 38 deletions

View File

@ -0,0 +1,150 @@
package net.corda.docs
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.Amount
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatedBy
import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.Party
import net.corda.core.node.PluginServiceHub
import net.corda.core.node.services.CordaService
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.loggerFor
import net.corda.core.utilities.unwrap
import net.corda.flows.*
import java.util.*
// DOCSTART CustomVaultQuery
object CustomVaultQuery {
@CordaService
class Service(val services: PluginServiceHub) : SingletonSerializeAsToken() {
private companion object {
val log = loggerFor<Service>()
}
fun rebalanceCurrencyReserves(): List<Amount<Currency>> {
val nativeQuery = """
select
cashschema.ccy_code,
sum(cashschema.pennies)
from
vault_states vaultschema
join
contract_cash_states cashschema
where
vaultschema.output_index=cashschema.output_index
and vaultschema.transaction_id=cashschema.transaction_id
and vaultschema.state_status=0
group by
cashschema.ccy_code
order by
sum(cashschema.pennies) desc
"""
log.info("SQL to execute: $nativeQuery")
val session = services.jdbcSession()
val prepStatement = session.prepareStatement(nativeQuery)
val rs = prepStatement.executeQuery()
var topUpLimits: MutableList<Amount<Currency>> = mutableListOf()
while (rs.next()) {
val currencyStr = rs.getString(1)
val amount = rs.getLong(2)
log.info("$currencyStr : $amount")
topUpLimits.add(Amount(amount, Currency.getInstance(currencyStr)))
}
return topUpLimits
}
}
}
// DOCEND CustomVaultQuery
/**
* This is a slightly modified version of the IssuerFlow, which uses a 3rd party custom query to
* retrieve a list of currencies and top up amounts to be used in the issuance.
*/
object TopupIssuerFlow {
@CordaSerializable
data class TopupRequest(val issueToParty: Party,
val issuerPartyRef: OpaqueBytes,
val notaryParty: Party)
@InitiatingFlow
@StartableByRPC
class TopupIssuanceRequester(val issueToParty: Party,
val issueToPartyRef: OpaqueBytes,
val issuerBankParty: Party,
val notaryParty: Party) : FlowLogic<List<AbstractCashFlow.Result>>() {
@Suspendable
@Throws(CashException::class)
override fun call(): List<AbstractCashFlow.Result> {
val topupRequest = TopupRequest(issueToParty, issueToPartyRef, notaryParty)
return sendAndReceive<List<AbstractCashFlow.Result>>(issuerBankParty, topupRequest).unwrap { it }
}
}
@InitiatedBy(TopupIssuanceRequester::class)
class TopupIssuer(val otherParty: Party) : FlowLogic<List<SignedTransaction>>() {
companion object {
object AWAITING_REQUEST : ProgressTracker.Step("Awaiting issuance request")
object ISSUING : ProgressTracker.Step("Issuing asset")
object TRANSFERRING : ProgressTracker.Step("Transferring asset to issuance requester")
object SENDING_TOP_UP_ISSUE_REQUEST : ProgressTracker.Step("Requesting asset issue top up")
fun tracker() = ProgressTracker(AWAITING_REQUEST, ISSUING, TRANSFERRING, SENDING_TOP_UP_ISSUE_REQUEST)
}
override val progressTracker: ProgressTracker = tracker()
// DOCSTART TopupIssuer
@Suspendable
@Throws(CashException::class)
override fun call(): List<SignedTransaction> {
progressTracker.currentStep = AWAITING_REQUEST
val topupRequest = receive<TopupRequest>(otherParty).unwrap {
it
}
val customVaultQueryService = serviceHub.cordaService(CustomVaultQuery.Service::class.java)
val reserveLimits = customVaultQueryService.rebalanceCurrencyReserves()
val txns: List<SignedTransaction> = reserveLimits.map { amount ->
// request asset issue
logger.info("Requesting currency issue $amount")
val txn = issueCashTo(amount, topupRequest.issueToParty, topupRequest.issuerPartyRef)
progressTracker.currentStep = SENDING_TOP_UP_ISSUE_REQUEST
return@map txn.stx
}
send(otherParty, txns)
return txns
}
// DOCEND TopupIssuer
@Suspendable
private fun issueCashTo(amount: Amount<Currency>,
issueTo: Party,
issuerPartyRef: OpaqueBytes): AbstractCashFlow.Result {
// TODO: pass notary in as request parameter
val notaryParty = serviceHub.networkMapCache.notaryNodes[0].notaryIdentity
// invoke Cash subflow to issue Asset
progressTracker.currentStep = ISSUING
val issueRecipient = serviceHub.myInfo.legalIdentity
val issueCashFlow = CashIssueFlow(amount, issuerPartyRef, issueRecipient, notaryParty, anonymous = false)
val issueTx = subFlow(issueCashFlow)
// NOTE: issueCashFlow performs a Broadcast (which stores a local copy of the txn to the ledger)
// short-circuit when issuing to self
if (issueTo == serviceHub.myInfo.legalIdentity)
return issueTx
// now invoke Cash subflow to Move issued assetType to issue requester
progressTracker.currentStep = TRANSFERRING
val moveCashFlow = CashPaymentFlow(amount, issueTo, anonymous = false)
val moveTx = subFlow(moveCashFlow)
// NOTE: CashFlow PayCash calls FinalityFlow which performs a Broadcast (which stores a local copy of the txn to the ledger)
return moveTx
}
}
}

View File

@ -0,0 +1,100 @@
package net.corda.docs
import net.corda.contracts.getCashBalances
import net.corda.core.contracts.*
import net.corda.core.getOrThrow
import net.corda.core.node.services.ServiceInfo
import net.corda.core.utilities.OpaqueBytes
import net.corda.flows.CashIssueFlow
import net.corda.node.services.network.NetworkMapService
import net.corda.node.services.transactions.ValidatingNotaryService
import net.corda.testing.DUMMY_NOTARY
import net.corda.testing.DUMMY_NOTARY_KEY
import net.corda.testing.node.MockNetwork
import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import java.util.*
class CustomVaultQueryTest {
lateinit var mockNet: MockNetwork
lateinit var notaryNode: MockNetwork.MockNode
lateinit var nodeA: MockNetwork.MockNode
lateinit var nodeB: MockNetwork.MockNode
@Before
fun setup() {
mockNet = MockNetwork(threadPerNode = true)
val notaryService = ServiceInfo(ValidatingNotaryService.type)
notaryNode = mockNet.createNode(
legalName = DUMMY_NOTARY.name,
overrideServices = mapOf(notaryService to DUMMY_NOTARY_KEY),
advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), notaryService))
nodeA = mockNet.createPartyNode(notaryNode.network.myAddress)
nodeB = mockNet.createPartyNode(notaryNode.network.myAddress)
nodeA.registerInitiatedFlow(TopupIssuerFlow.TopupIssuer::class.java)
nodeA.installCordaService(CustomVaultQuery.Service::class.java)
}
@After
fun cleanUp() {
mockNet.stopNodes()
}
@Test
fun `test custom vault query`() {
// issue some cash in several currencies
issueCashForCurrency(POUNDS(1000))
issueCashForCurrency(DOLLARS(900))
issueCashForCurrency(SWISS_FRANCS(800))
val (cashBalancesOriginal, _) = getBalances()
// top up all currencies (by double original)
topUpCurrencies()
val (cashBalancesAfterTopup, _) = getBalances()
Assert.assertEquals(cashBalancesOriginal[GBP]?.times(2), cashBalancesAfterTopup[GBP])
Assert.assertEquals(cashBalancesOriginal[USD]?.times(2) , cashBalancesAfterTopup[USD])
Assert.assertEquals(cashBalancesOriginal[CHF]?.times( 2), cashBalancesAfterTopup[CHF])
}
private fun issueCashForCurrency(amountToIssue: Amount<Currency>) {
// Use NodeA as issuer and create some dollars
val flowHandle1 = nodeA.services.startFlow(CashIssueFlow(amountToIssue,
OpaqueBytes.of(0x01),
nodeA.info.legalIdentity,
notaryNode.info.notaryIdentity,
false))
// Wait for the flow to stop and print
flowHandle1.resultFuture.getOrThrow()
}
private fun topUpCurrencies() {
val flowHandle1 = nodeA.services.startFlow(TopupIssuerFlow.TopupIssuanceRequester(
nodeA.info.legalIdentity,
OpaqueBytes.of(0x01),
nodeA.info.legalIdentity,
notaryNode.info.notaryIdentity))
flowHandle1.resultFuture.getOrThrow()
}
private fun getBalances(): Pair<Map<Currency, Amount<Currency>>, Map<Currency, Amount<Currency>>> {
// Print out the balances
val balancesNodesA =
nodeA.database.transaction {
nodeA.services.getCashBalances()
}
println("BalanceA\n" + balancesNodesA)
val balancesNodesB =
nodeB.database.transaction {
nodeB.services.getCashBalances()
}
println("BalanceB\n" + balancesNodesB)
return Pair(balancesNodesA, balancesNodesB)
}
}