mirror of
https://github.com/corda/corda.git
synced 2024-12-24 07:06:44 +00:00
Merged in aslemmer-generic-protocol-start (pull request #465)
Aslemmer generic protocol start
This commit is contained in:
commit
9078676521
@ -3,7 +3,10 @@ package net.corda.client
|
|||||||
import net.corda.client.model.NodeMonitorModel
|
import net.corda.client.model.NodeMonitorModel
|
||||||
import net.corda.client.model.ProgressTrackingEvent
|
import net.corda.client.model.ProgressTrackingEvent
|
||||||
import net.corda.core.bufferUntilSubscribed
|
import net.corda.core.bufferUntilSubscribed
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.Amount
|
||||||
|
import net.corda.core.contracts.Issued
|
||||||
|
import net.corda.core.contracts.PartyAndReference
|
||||||
|
import net.corda.core.contracts.USD
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.core.node.services.NetworkMapCache
|
import net.corda.core.node.services.NetworkMapCache
|
||||||
import net.corda.core.node.services.ServiceInfo
|
import net.corda.core.node.services.ServiceInfo
|
||||||
@ -13,13 +16,15 @@ import net.corda.core.protocols.StateMachineRunId
|
|||||||
import net.corda.core.serialization.OpaqueBytes
|
import net.corda.core.serialization.OpaqueBytes
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.node.driver.driver
|
import net.corda.node.driver.driver
|
||||||
import net.corda.node.internal.CordaRPCOpsImpl
|
|
||||||
import net.corda.node.services.User
|
import net.corda.node.services.User
|
||||||
import net.corda.node.services.config.configureTestSSL
|
import net.corda.node.services.config.configureTestSSL
|
||||||
import net.corda.node.services.messaging.ArtemisMessagingComponent
|
import net.corda.node.services.messaging.ArtemisMessagingComponent
|
||||||
import net.corda.node.services.messaging.StateMachineUpdate
|
import net.corda.node.services.messaging.StateMachineUpdate
|
||||||
import net.corda.node.services.network.NetworkMapService
|
import net.corda.node.services.network.NetworkMapService
|
||||||
|
import net.corda.node.services.startProtocolPermission
|
||||||
import net.corda.node.services.transactions.SimpleNotaryService
|
import net.corda.node.services.transactions.SimpleNotaryService
|
||||||
|
import net.corda.protocols.CashCommand
|
||||||
|
import net.corda.protocols.CashProtocol
|
||||||
import net.corda.testing.expect
|
import net.corda.testing.expect
|
||||||
import net.corda.testing.expectEvents
|
import net.corda.testing.expectEvents
|
||||||
import net.corda.testing.sequence
|
import net.corda.testing.sequence
|
||||||
@ -43,7 +48,7 @@ class NodeMonitorModelTest {
|
|||||||
lateinit var transactions: Observable<SignedTransaction>
|
lateinit var transactions: Observable<SignedTransaction>
|
||||||
lateinit var vaultUpdates: Observable<Vault.Update>
|
lateinit var vaultUpdates: Observable<Vault.Update>
|
||||||
lateinit var networkMapUpdates: Observable<NetworkMapCache.MapChange>
|
lateinit var networkMapUpdates: Observable<NetworkMapCache.MapChange>
|
||||||
lateinit var clientToService: Observer<ClientToServiceCommand>
|
lateinit var clientToService: Observer<CashCommand>
|
||||||
lateinit var newNode: (String) -> NodeInfo
|
lateinit var newNode: (String) -> NodeInfo
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
@ -51,7 +56,7 @@ class NodeMonitorModelTest {
|
|||||||
val driverStarted = CountDownLatch(1)
|
val driverStarted = CountDownLatch(1)
|
||||||
driverThread = thread {
|
driverThread = thread {
|
||||||
driver {
|
driver {
|
||||||
val cashUser = User("user1", "test", permissions = setOf(CordaRPCOpsImpl.CASH_PERMISSION))
|
val cashUser = User("user1", "test", permissions = setOf(startProtocolPermission<CashProtocol>()))
|
||||||
val aliceNodeFuture = startNode("Alice", rpcUsers = listOf(cashUser))
|
val aliceNodeFuture = startNode("Alice", rpcUsers = listOf(cashUser))
|
||||||
val notaryNodeFuture = startNode("Notary", advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
|
val notaryNodeFuture = startNode("Notary", advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
|
||||||
|
|
||||||
@ -106,7 +111,7 @@ class NodeMonitorModelTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `cash issue works end to end`() {
|
fun `cash issue works end to end`() {
|
||||||
clientToService.onNext(ClientToServiceCommand.IssueCash(
|
clientToService.onNext(CashCommand.IssueCash(
|
||||||
amount = Amount(100, USD),
|
amount = Amount(100, USD),
|
||||||
issueRef = OpaqueBytes(ByteArray(1, { 1 })),
|
issueRef = OpaqueBytes(ByteArray(1, { 1 })),
|
||||||
recipient = aliceNode.legalIdentity,
|
recipient = aliceNode.legalIdentity,
|
||||||
@ -131,14 +136,14 @@ class NodeMonitorModelTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `cash issue and move`() {
|
fun `cash issue and move`() {
|
||||||
clientToService.onNext(ClientToServiceCommand.IssueCash(
|
clientToService.onNext(CashCommand.IssueCash(
|
||||||
amount = Amount(100, USD),
|
amount = Amount(100, USD),
|
||||||
issueRef = OpaqueBytes(ByteArray(1, { 1 })),
|
issueRef = OpaqueBytes(ByteArray(1, { 1 })),
|
||||||
recipient = aliceNode.legalIdentity,
|
recipient = aliceNode.legalIdentity,
|
||||||
notary = notaryNode.notaryIdentity
|
notary = notaryNode.notaryIdentity
|
||||||
))
|
))
|
||||||
|
|
||||||
clientToService.onNext(ClientToServiceCommand.PayCash(
|
clientToService.onNext(CashCommand.PayCash(
|
||||||
amount = Amount(100, Issued(PartyAndReference(aliceNode.legalIdentity, OpaqueBytes(ByteArray(1, { 1 }))), USD)),
|
amount = Amount(100, Issued(PartyAndReference(aliceNode.legalIdentity, OpaqueBytes(ByteArray(1, { 1 }))), USD)),
|
||||||
recipient = aliceNode.legalIdentity
|
recipient = aliceNode.legalIdentity
|
||||||
))
|
))
|
||||||
|
@ -5,7 +5,7 @@ import net.corda.core.contracts.*
|
|||||||
import net.corda.core.crypto.Party
|
import net.corda.core.crypto.Party
|
||||||
import net.corda.core.serialization.OpaqueBytes
|
import net.corda.core.serialization.OpaqueBytes
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
import java.time.Instant
|
import net.corda.protocols.CashCommand
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [Generator]s for incoming/outgoing events to/from the [WalletMonitorService]. Internally it keeps track of owned
|
* [Generator]s for incoming/outgoing events to/from the [WalletMonitorService]. Internally it keeps track of owned
|
||||||
@ -65,7 +65,7 @@ class EventGenerator(
|
|||||||
|
|
||||||
val issueCashGenerator =
|
val issueCashGenerator =
|
||||||
amountGenerator.combine(partyGenerator, issueRefGenerator) { amount, to, issueRef ->
|
amountGenerator.combine(partyGenerator, issueRefGenerator) { amount, to, issueRef ->
|
||||||
ClientToServiceCommand.IssueCash(
|
CashCommand.IssueCash(
|
||||||
amount,
|
amount,
|
||||||
issueRef,
|
issueRef,
|
||||||
to,
|
to,
|
||||||
@ -77,7 +77,7 @@ class EventGenerator(
|
|||||||
amountIssuedGenerator.combine(
|
amountIssuedGenerator.combine(
|
||||||
partyGenerator
|
partyGenerator
|
||||||
) { amountIssued, recipient ->
|
) { amountIssued, recipient ->
|
||||||
ClientToServiceCommand.PayCash(
|
CashCommand.PayCash(
|
||||||
amount = amountIssued,
|
amount = amountIssued,
|
||||||
recipient = recipient
|
recipient = recipient
|
||||||
)
|
)
|
||||||
@ -85,7 +85,7 @@ class EventGenerator(
|
|||||||
|
|
||||||
val exitCashGenerator =
|
val exitCashGenerator =
|
||||||
amountIssuedGenerator.map {
|
amountIssuedGenerator.map {
|
||||||
ClientToServiceCommand.ExitCash(
|
CashCommand.ExitCash(
|
||||||
it.withoutIssuer(),
|
it.withoutIssuer(),
|
||||||
it.token.issuer.reference
|
it.token.issuer.reference
|
||||||
)
|
)
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
package net.corda.client.model
|
package net.corda.client.model
|
||||||
|
|
||||||
import com.google.common.net.HostAndPort
|
import com.google.common.net.HostAndPort
|
||||||
|
import javafx.beans.property.SimpleObjectProperty
|
||||||
import net.corda.client.CordaRPCClient
|
import net.corda.client.CordaRPCClient
|
||||||
import net.corda.core.contracts.ClientToServiceCommand
|
|
||||||
import net.corda.core.node.services.NetworkMapCache
|
import net.corda.core.node.services.NetworkMapCache
|
||||||
import net.corda.core.node.services.StateMachineTransactionMapping
|
import net.corda.core.node.services.StateMachineTransactionMapping
|
||||||
import net.corda.core.node.services.Vault
|
import net.corda.core.node.services.Vault
|
||||||
@ -12,7 +12,9 @@ import net.corda.node.services.config.NodeSSLConfiguration
|
|||||||
import net.corda.node.services.messaging.CordaRPCOps
|
import net.corda.node.services.messaging.CordaRPCOps
|
||||||
import net.corda.node.services.messaging.StateMachineInfo
|
import net.corda.node.services.messaging.StateMachineInfo
|
||||||
import net.corda.node.services.messaging.StateMachineUpdate
|
import net.corda.node.services.messaging.StateMachineUpdate
|
||||||
import javafx.beans.property.SimpleObjectProperty
|
import net.corda.node.services.messaging.startProtocol
|
||||||
|
import net.corda.protocols.CashCommand
|
||||||
|
import net.corda.protocols.CashProtocol
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.subjects.PublishSubject
|
import rx.subjects.PublishSubject
|
||||||
|
|
||||||
@ -46,8 +48,8 @@ class NodeMonitorModel {
|
|||||||
val progressTracking: Observable<ProgressTrackingEvent> = progressTrackingSubject
|
val progressTracking: Observable<ProgressTrackingEvent> = progressTrackingSubject
|
||||||
val networkMap: Observable<NetworkMapCache.MapChange> = networkMapSubject
|
val networkMap: Observable<NetworkMapCache.MapChange> = networkMapSubject
|
||||||
|
|
||||||
private val clientToServiceSource = PublishSubject.create<ClientToServiceCommand>()
|
private val clientToServiceSource = PublishSubject.create<CashCommand>()
|
||||||
val clientToService: PublishSubject<ClientToServiceCommand> = clientToServiceSource
|
val clientToService: PublishSubject<CashCommand> = clientToServiceSource
|
||||||
|
|
||||||
val proxyObservable = SimpleObjectProperty<CordaRPCOps?>()
|
val proxyObservable = SimpleObjectProperty<CordaRPCOps?>()
|
||||||
|
|
||||||
@ -98,7 +100,7 @@ class NodeMonitorModel {
|
|||||||
|
|
||||||
// Client -> Service
|
// Client -> Service
|
||||||
clientToServiceSource.subscribe {
|
clientToServiceSource.subscribe {
|
||||||
proxy.executeCommand(it)
|
proxy.startProtocol(::CashProtocol, it)
|
||||||
}
|
}
|
||||||
proxyObservable.set(proxy)
|
proxyObservable.set(proxy)
|
||||||
}
|
}
|
||||||
|
@ -69,7 +69,7 @@ class ClientRPCInfrastructureTests {
|
|||||||
serverSession.createTemporaryQueue(RPC_REQUESTS_QUEUE, RPC_REQUESTS_QUEUE)
|
serverSession.createTemporaryQueue(RPC_REQUESTS_QUEUE, RPC_REQUESTS_QUEUE)
|
||||||
producer = serverSession.createProducer()
|
producer = serverSession.createProducer()
|
||||||
val userService = object : RPCUserService {
|
val userService = object : RPCUserService {
|
||||||
override fun getUser(usename: String): User? = throw UnsupportedOperationException()
|
override fun getUser(username: String): User? = throw UnsupportedOperationException()
|
||||||
override val users: List<User> get() = throw UnsupportedOperationException()
|
override val users: List<User> get() = throw UnsupportedOperationException()
|
||||||
}
|
}
|
||||||
val dispatcher = object : RPCDispatcher(TestOpsImpl(), userService) {
|
val dispatcher = object : RPCDispatcher(TestOpsImpl(), userService) {
|
||||||
|
@ -99,6 +99,21 @@ inline fun <T> SettableFuture<T>.catch(block: () -> T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun <A> ListenableFuture<A>.toObservable(): Observable<A> {
|
||||||
|
return Observable.create { subscriber ->
|
||||||
|
then {
|
||||||
|
try {
|
||||||
|
subscriber.onNext(get())
|
||||||
|
subscriber.onCompleted()
|
||||||
|
} catch (e: ExecutionException) {
|
||||||
|
subscriber.onError(e.cause!!)
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
subscriber.onError(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Allows you to write code like: Paths.get("someDir") / "subdir" / "filename" but using the Paths API to avoid platform separator problems. */
|
/** Allows you to write code like: Paths.get("someDir") / "subdir" / "filename" but using the Paths API to avoid platform separator problems. */
|
||||||
operator fun Path.div(other: String): Path = resolve(other)
|
operator fun Path.div(other: String): Path = resolve(other)
|
||||||
fun Path.createDirectory(vararg attrs: FileAttribute<*>): Path = Files.createDirectory(this, *attrs)
|
fun Path.createDirectory(vararg attrs: FileAttribute<*>): Path = Files.createDirectory(this, *attrs)
|
||||||
|
@ -1,49 +0,0 @@
|
|||||||
package net.corda.core.contracts
|
|
||||||
|
|
||||||
import net.corda.core.crypto.Party
|
|
||||||
import net.corda.core.serialization.OpaqueBytes
|
|
||||||
import java.security.PublicKey
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A command from the monitoring client, to the node.
|
|
||||||
*
|
|
||||||
* @param id ID used to tag event(s) resulting from a command.
|
|
||||||
*/
|
|
||||||
sealed class ClientToServiceCommand(val id: UUID) {
|
|
||||||
/**
|
|
||||||
* Issue cash state objects.
|
|
||||||
*
|
|
||||||
* @param amount the amount of currency to issue on to the ledger.
|
|
||||||
* @param issueRef the reference to specify on the issuance, used to differentiate pools of cash. Convention is
|
|
||||||
* to use the single byte "0x01" as a default.
|
|
||||||
* @param recipient the party to issue the cash to.
|
|
||||||
* @param notary the notary to use for this transaction.
|
|
||||||
* @param id the ID to be provided in events resulting from this request.
|
|
||||||
*/
|
|
||||||
class IssueCash(val amount: Amount<Currency>,
|
|
||||||
val issueRef: OpaqueBytes,
|
|
||||||
val recipient: Party,
|
|
||||||
val notary: Party,
|
|
||||||
id: UUID = UUID.randomUUID()) : ClientToServiceCommand(id)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pay cash to someone else.
|
|
||||||
*
|
|
||||||
* @param amount the amount of currency to issue on to the ledger.
|
|
||||||
* @param recipient the party to issue the cash to.
|
|
||||||
* @param id the ID to be provided in events resulting from this request.
|
|
||||||
*/
|
|
||||||
class PayCash(val amount: Amount<Issued<Currency>>, val recipient: Party,
|
|
||||||
id: UUID = UUID.randomUUID()) : ClientToServiceCommand(id)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exit cash from the ledger.
|
|
||||||
*
|
|
||||||
* @param amount the amount of currency to exit from the ledger.
|
|
||||||
* @param issueRef the reference previously specified on the issuance.
|
|
||||||
* @param id the ID to be provided in events resulting from this request.
|
|
||||||
*/
|
|
||||||
class ExitCash(val amount: Amount<Currency>, val issueRef: OpaqueBytes,
|
|
||||||
id: UUID = UUID.randomUUID()) : ClientToServiceCommand(id)
|
|
||||||
}
|
|
@ -1,12 +1,12 @@
|
|||||||
package net.corda.core.node
|
package net.corda.core.node
|
||||||
|
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
|
||||||
import net.corda.core.contracts.StateRef
|
import net.corda.core.contracts.StateRef
|
||||||
import net.corda.core.contracts.TransactionResolutionException
|
import net.corda.core.contracts.TransactionResolutionException
|
||||||
import net.corda.core.contracts.TransactionState
|
import net.corda.core.contracts.TransactionState
|
||||||
import net.corda.core.messaging.MessagingService
|
import net.corda.core.messaging.MessagingService
|
||||||
import net.corda.core.node.services.*
|
import net.corda.core.node.services.*
|
||||||
import net.corda.core.protocols.ProtocolLogic
|
import net.corda.core.protocols.ProtocolLogic
|
||||||
|
import net.corda.core.protocols.ProtocolStateMachine
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import java.security.KeyPair
|
import java.security.KeyPair
|
||||||
import java.time.Clock
|
import java.time.Clock
|
||||||
@ -53,7 +53,7 @@ interface ServiceHub {
|
|||||||
*
|
*
|
||||||
* @throws IllegalProtocolLogicException or IllegalArgumentException if there are problems with the [logicType] or [args].
|
* @throws IllegalProtocolLogicException or IllegalArgumentException if there are problems with the [logicType] or [args].
|
||||||
*/
|
*/
|
||||||
fun <T : Any> invokeProtocolAsync(logicType: Class<out ProtocolLogic<T>>, vararg args: Any?): ListenableFuture<T>
|
fun <T : Any> invokeProtocolAsync(logicType: Class<out ProtocolLogic<T>>, vararg args: Any?): ProtocolStateMachine<T>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper property to shorten code for fetching the Node's KeyPair associated with the
|
* Helper property to shorten code for fetching the Node's KeyPair associated with the
|
||||||
|
@ -46,7 +46,7 @@ class ProtocolLogicRefFactory(private val protocolWhitelist: Map<String, Set<Str
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// TODO: make this specific to the attachments in the [AppContext] by including [SecureHash] in whitelist check
|
// TODO: make this specific to the attachments in the [AppContext] by including [SecureHash] in whitelist check
|
||||||
require(protocolWhitelist[className]!!.contains(argClassName)) { "Args to $className must have types on the args whitelist: $argClassName" }
|
require(protocolWhitelist[className]!!.contains(argClassName)) { "Args to $className must have types on the args whitelist: $argClassName, but it has ${protocolWhitelist[className]}" }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package net.corda.protocols
|
package net.corda.protocols
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import net.corda.core.contracts.ClientToServiceCommand
|
|
||||||
import net.corda.core.crypto.Party
|
import net.corda.core.crypto.Party
|
||||||
import net.corda.core.node.recordTransactions
|
import net.corda.core.node.recordTransactions
|
||||||
import net.corda.core.protocols.ProtocolLogic
|
import net.corda.core.protocols.ProtocolLogic
|
||||||
@ -13,19 +12,13 @@ import net.corda.core.transactions.SignedTransaction
|
|||||||
* [FinalityProtocol].
|
* [FinalityProtocol].
|
||||||
*
|
*
|
||||||
* @param notarisedTransaction transaction which has been notarised (if needed) and is ready to notify nodes about.
|
* @param notarisedTransaction transaction which has been notarised (if needed) and is ready to notify nodes about.
|
||||||
* @param events information on the event(s) which triggered the transaction.
|
|
||||||
* @param participants a list of participants involved in the transaction.
|
* @param participants a list of participants involved in the transaction.
|
||||||
* @return a list of participants who were successfully notified of the transaction.
|
* @return a list of participants who were successfully notified of the transaction.
|
||||||
*/
|
*/
|
||||||
// TODO: Event needs to be replaced with something that's meaningful, but won't ever contain sensitive
|
|
||||||
// information (such as internal details of an account to take payment from). Suggest
|
|
||||||
// splitting ClientToServiceCommand into public and private parts, with only the public parts
|
|
||||||
// relayed here.
|
|
||||||
class BroadcastTransactionProtocol(val notarisedTransaction: SignedTransaction,
|
class BroadcastTransactionProtocol(val notarisedTransaction: SignedTransaction,
|
||||||
val events: Set<ClientToServiceCommand>,
|
|
||||||
val participants: Set<Party>) : ProtocolLogic<Unit>() {
|
val participants: Set<Party>) : ProtocolLogic<Unit>() {
|
||||||
|
|
||||||
data class NotifyTxRequest(val tx: SignedTransaction, val events: Set<ClientToServiceCommand>)
|
data class NotifyTxRequest(val tx: SignedTransaction)
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call() {
|
override fun call() {
|
||||||
@ -33,7 +26,7 @@ class BroadcastTransactionProtocol(val notarisedTransaction: SignedTransaction,
|
|||||||
serviceHub.recordTransactions(notarisedTransaction)
|
serviceHub.recordTransactions(notarisedTransaction)
|
||||||
|
|
||||||
// TODO: Messaging layer should handle this broadcast for us
|
// TODO: Messaging layer should handle this broadcast for us
|
||||||
val msg = NotifyTxRequest(notarisedTransaction, events)
|
val msg = NotifyTxRequest(notarisedTransaction)
|
||||||
participants.filter { it != serviceHub.myInfo.legalIdentity }.forEach { participant ->
|
participants.filter { it != serviceHub.myInfo.legalIdentity }.forEach { participant ->
|
||||||
send(participant, msg)
|
send(participant, msg)
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package net.corda.protocols
|
package net.corda.protocols
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import net.corda.core.contracts.ClientToServiceCommand
|
|
||||||
import net.corda.core.crypto.Party
|
import net.corda.core.crypto.Party
|
||||||
import net.corda.core.protocols.ProtocolLogic
|
import net.corda.core.protocols.ProtocolLogic
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
@ -12,16 +11,10 @@ import net.corda.core.utilities.ProgressTracker
|
|||||||
* Finalise a transaction by notarising it, then recording it locally, and then sending it to all involved parties.
|
* Finalise a transaction by notarising it, then recording it locally, and then sending it to all involved parties.
|
||||||
*
|
*
|
||||||
* @param transaction to commit.
|
* @param transaction to commit.
|
||||||
* @param events information on the event(s) which triggered the transaction.
|
|
||||||
* @param participants a list of participants involved in the transaction.
|
* @param participants a list of participants involved in the transaction.
|
||||||
* @return a list of participants who were successfully notified of the transaction.
|
* @return a list of participants who were successfully notified of the transaction.
|
||||||
*/
|
*/
|
||||||
// TODO: Event needs to be replaced with something that's meaningful, but won't ever contain sensitive
|
|
||||||
// information (such as internal details of an account to take payment from). Suggest
|
|
||||||
// splitting ClientToServiceCommand into public and private parts, with only the public parts
|
|
||||||
// relayed here.
|
|
||||||
class FinalityProtocol(val transaction: SignedTransaction,
|
class FinalityProtocol(val transaction: SignedTransaction,
|
||||||
val events: Set<ClientToServiceCommand>,
|
|
||||||
val participants: Set<Party>,
|
val participants: Set<Party>,
|
||||||
override val progressTracker: ProgressTracker = tracker()): ProtocolLogic<Unit>() {
|
override val progressTracker: ProgressTracker = tracker()): ProtocolLogic<Unit>() {
|
||||||
companion object {
|
companion object {
|
||||||
@ -46,7 +39,7 @@ class FinalityProtocol(val transaction: SignedTransaction,
|
|||||||
|
|
||||||
// Let everyone else know about the transaction
|
// Let everyone else know about the transaction
|
||||||
progressTracker.currentStep = BROADCASTING
|
progressTracker.currentStep = BROADCASTING
|
||||||
subProtocol(BroadcastTransactionProtocol(notarisedTransaction, events, participants))
|
subProtocol(BroadcastTransactionProtocol(notarisedTransaction, participants))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun needsNotarySignature(stx: SignedTransaction) = stx.tx.notary != null && hasNoNotarySignature(stx)
|
private fun needsNotarySignature(stx: SignedTransaction) = stx.tx.notary != null && hasNoNotarySignature(stx)
|
||||||
|
@ -19,7 +19,7 @@ class BroadcastTransactionProtocolTest {
|
|||||||
|
|
||||||
class NotifyTxRequestMessageGenerator : Generator<NotifyTxRequest>(NotifyTxRequest::class.java) {
|
class NotifyTxRequestMessageGenerator : Generator<NotifyTxRequest>(NotifyTxRequest::class.java) {
|
||||||
override fun generate(random: SourceOfRandomness, status: GenerationStatus): NotifyTxRequest {
|
override fun generate(random: SourceOfRandomness, status: GenerationStatus): NotifyTxRequest {
|
||||||
return NotifyTxRequest(tx = SignedTransactionGenerator().generate(random, status), events = setOf())
|
return NotifyTxRequest(tx = SignedTransactionGenerator().generate(random, status))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ class ResolveTransactionsProtocolTest {
|
|||||||
fun `resolve from two hashes`() {
|
fun `resolve from two hashes`() {
|
||||||
val (stx1, stx2) = makeTransactions()
|
val (stx1, stx2) = makeTransactions()
|
||||||
val p = ResolveTransactionsProtocol(setOf(stx2.id), a.info.legalIdentity)
|
val p = ResolveTransactionsProtocol(setOf(stx2.id), a.info.legalIdentity)
|
||||||
val future = b.services.startProtocol(p)
|
val future = b.services.startProtocol(p).resultFuture
|
||||||
net.runNetwork()
|
net.runNetwork()
|
||||||
val results = future.get()
|
val results = future.get()
|
||||||
assertEquals(listOf(stx1.id, stx2.id), results.map { it.id })
|
assertEquals(listOf(stx1.id, stx2.id), results.map { it.id })
|
||||||
@ -63,7 +63,7 @@ class ResolveTransactionsProtocolTest {
|
|||||||
fun `dependency with an error`() {
|
fun `dependency with an error`() {
|
||||||
val stx = makeTransactions(signFirstTX = false).second
|
val stx = makeTransactions(signFirstTX = false).second
|
||||||
val p = ResolveTransactionsProtocol(setOf(stx.id), a.info.legalIdentity)
|
val p = ResolveTransactionsProtocol(setOf(stx.id), a.info.legalIdentity)
|
||||||
val future = b.services.startProtocol(p)
|
val future = b.services.startProtocol(p).resultFuture
|
||||||
net.runNetwork()
|
net.runNetwork()
|
||||||
assertFailsWith(SignatureException::class) {
|
assertFailsWith(SignatureException::class) {
|
||||||
rootCauseExceptions { future.get() }
|
rootCauseExceptions { future.get() }
|
||||||
@ -74,7 +74,7 @@ class ResolveTransactionsProtocolTest {
|
|||||||
fun `resolve from a signed transaction`() {
|
fun `resolve from a signed transaction`() {
|
||||||
val (stx1, stx2) = makeTransactions()
|
val (stx1, stx2) = makeTransactions()
|
||||||
val p = ResolveTransactionsProtocol(stx2, a.info.legalIdentity)
|
val p = ResolveTransactionsProtocol(stx2, a.info.legalIdentity)
|
||||||
val future = b.services.startProtocol(p)
|
val future = b.services.startProtocol(p).resultFuture
|
||||||
net.runNetwork()
|
net.runNetwork()
|
||||||
future.get()
|
future.get()
|
||||||
databaseTransaction(b.database) {
|
databaseTransaction(b.database) {
|
||||||
@ -101,7 +101,7 @@ class ResolveTransactionsProtocolTest {
|
|||||||
}
|
}
|
||||||
val p = ResolveTransactionsProtocol(setOf(cursor.id), a.info.legalIdentity)
|
val p = ResolveTransactionsProtocol(setOf(cursor.id), a.info.legalIdentity)
|
||||||
p.transactionCountLimit = 40
|
p.transactionCountLimit = 40
|
||||||
val future = b.services.startProtocol(p)
|
val future = b.services.startProtocol(p).resultFuture
|
||||||
net.runNetwork()
|
net.runNetwork()
|
||||||
assertFailsWith<ResolveTransactionsProtocol.ExcessivelyLargeTransactionGraph> {
|
assertFailsWith<ResolveTransactionsProtocol.ExcessivelyLargeTransactionGraph> {
|
||||||
rootCauseExceptions { future.get() }
|
rootCauseExceptions { future.get() }
|
||||||
@ -129,7 +129,7 @@ class ResolveTransactionsProtocolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val p = ResolveTransactionsProtocol(setOf(stx3.id), a.info.legalIdentity)
|
val p = ResolveTransactionsProtocol(setOf(stx3.id), a.info.legalIdentity)
|
||||||
val future = b.services.startProtocol(p)
|
val future = b.services.startProtocol(p).resultFuture
|
||||||
net.runNetwork()
|
net.runNetwork()
|
||||||
future.get()
|
future.get()
|
||||||
}
|
}
|
||||||
@ -139,7 +139,7 @@ class ResolveTransactionsProtocolTest {
|
|||||||
val id = a.services.storageService.attachments.importAttachment("Some test file".toByteArray().opaque().open())
|
val id = a.services.storageService.attachments.importAttachment("Some test file".toByteArray().opaque().open())
|
||||||
val stx2 = makeTransactions(withAttachment = id).second
|
val stx2 = makeTransactions(withAttachment = id).second
|
||||||
val p = ResolveTransactionsProtocol(stx2, a.info.legalIdentity)
|
val p = ResolveTransactionsProtocol(stx2, a.info.legalIdentity)
|
||||||
val future = b.services.startProtocol(p)
|
val future = b.services.startProtocol(p).resultFuture
|
||||||
net.runNetwork()
|
net.runNetwork()
|
||||||
future.get()
|
future.get()
|
||||||
assertNotNull(b.services.storageService.attachments.openAttachment(id))
|
assertNotNull(b.services.storageService.attachments.openAttachment(id))
|
||||||
|
@ -19,6 +19,14 @@ repositories {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
main {
|
||||||
|
resources {
|
||||||
|
srcDir "../../../config/dev"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile project(':core')
|
compile project(':core')
|
||||||
compile project(':client')
|
compile project(':client')
|
||||||
|
@ -1,15 +1,32 @@
|
|||||||
package net.corda.docs
|
package net.corda.docs
|
||||||
|
|
||||||
import com.google.common.net.HostAndPort
|
|
||||||
import net.corda.client.CordaRPCClient
|
import net.corda.client.CordaRPCClient
|
||||||
|
import net.corda.contracts.asset.Cash
|
||||||
|
import net.corda.core.contracts.Amount
|
||||||
|
import net.corda.core.contracts.Issued
|
||||||
|
import net.corda.core.contracts.PartyAndReference
|
||||||
|
import net.corda.core.contracts.USD
|
||||||
|
import net.corda.core.div
|
||||||
|
import net.corda.core.node.services.ServiceInfo
|
||||||
|
import net.corda.core.serialization.OpaqueBytes
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
|
import net.corda.node.driver.driver
|
||||||
|
import net.corda.node.services.User
|
||||||
|
import net.corda.node.services.config.FullNodeConfiguration
|
||||||
import net.corda.node.services.config.NodeSSLConfiguration
|
import net.corda.node.services.config.NodeSSLConfiguration
|
||||||
|
import net.corda.node.services.messaging.CordaRPCOps
|
||||||
|
import net.corda.node.services.messaging.startProtocol
|
||||||
|
import net.corda.node.services.startProtocolPermission
|
||||||
|
import net.corda.node.services.transactions.ValidatingNotaryService
|
||||||
|
import net.corda.protocols.CashCommand
|
||||||
|
import net.corda.protocols.CashProtocol
|
||||||
import org.graphstream.graph.Edge
|
import org.graphstream.graph.Edge
|
||||||
import org.graphstream.graph.Node
|
import org.graphstream.graph.Node
|
||||||
import org.graphstream.graph.implementations.SingleGraph
|
import org.graphstream.graph.implementations.MultiGraph
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.*
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is example code for the Client RPC API tutorial. The START/END comments are important and used by the documentation!
|
* This is example code for the Client RPC API tutorial. The START/END comments are important and used by the documentation!
|
||||||
@ -22,61 +39,100 @@ enum class PrintOrVisualise {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
if (args.size < 2) {
|
if (args.size < 1) {
|
||||||
throw IllegalArgumentException("Usage: <binary> <node address> [Print|Visualise]")
|
throw IllegalArgumentException("Usage: <binary> [Print|Visualise]")
|
||||||
}
|
}
|
||||||
val nodeAddress = HostAndPort.fromString(args[0])
|
val printOrVisualise = PrintOrVisualise.valueOf(args[0])
|
||||||
val printOrVisualise = PrintOrVisualise.valueOf(args[1])
|
|
||||||
val sslConfig = object : NodeSSLConfiguration {
|
|
||||||
override val certificatesPath = Paths.get("build/trader-demo/buyer/certificates")
|
|
||||||
override val keyStorePassword = "cordacadevpass"
|
|
||||||
override val trustStorePassword = "trustpass"
|
|
||||||
}
|
|
||||||
// END 1
|
|
||||||
|
|
||||||
// START 2
|
val baseDirectory = Paths.get("build/rpc-api-tutorial")
|
||||||
val username = System.console().readLine("Enter username: ")
|
val user = User("user", "password", permissions = setOf(startProtocolPermission<CashProtocol>()))
|
||||||
val password = String(System.console().readPassword("Enter password: "))
|
|
||||||
val client = CordaRPCClient(nodeAddress, sslConfig)
|
|
||||||
client.start(username, password)
|
|
||||||
val proxy = client.proxy()
|
|
||||||
// END 2
|
|
||||||
|
|
||||||
// START 3
|
driver(driverDirectory = baseDirectory) {
|
||||||
val (transactions: List<SignedTransaction>, futureTransactions: Observable<SignedTransaction>) = proxy.verifiedTransactions()
|
startNode("Notary", advertisedServices = setOf(ServiceInfo(ValidatingNotaryService.type)))
|
||||||
// END 3
|
val node = startNode("Alice", rpcUsers = listOf(user)).get()
|
||||||
|
val sslConfig = object : NodeSSLConfiguration {
|
||||||
// START 4
|
override val certificatesPath = baseDirectory / "Alice" / "certificates"
|
||||||
when (printOrVisualise) {
|
override val keyStorePassword = "cordacadevpass"
|
||||||
PrintOrVisualise.Print -> {
|
override val trustStorePassword = "trustpass"
|
||||||
futureTransactions.startWith(transactions).subscribe { transaction ->
|
|
||||||
println("NODE ${transaction.id}")
|
|
||||||
transaction.tx.inputs.forEach { input ->
|
|
||||||
println("EDGE ${input.txhash} ${transaction.id}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CompletableFuture<Unit>().get() // block indefinitely
|
|
||||||
}
|
}
|
||||||
// END 4
|
// END 1
|
||||||
// START 5
|
|
||||||
PrintOrVisualise.Visualise -> {
|
// START 2
|
||||||
val graph = SingleGraph("transactions")
|
val client = CordaRPCClient(FullNodeConfiguration(node.config).artemisAddress, sslConfig)
|
||||||
transactions.forEach { transaction ->
|
client.start("user", "password")
|
||||||
graph.addNode<Node>("${transaction.id}")
|
val proxy = client.proxy()
|
||||||
}
|
|
||||||
transactions.forEach { transaction ->
|
thread {
|
||||||
transaction.tx.inputs.forEach { ref ->
|
generateTransactions(proxy)
|
||||||
graph.addEdge<Edge>("$ref", "${ref.txhash}", "${transaction.id}")
|
}
|
||||||
|
// END 2
|
||||||
|
|
||||||
|
// START 3
|
||||||
|
val (transactions: List<SignedTransaction>, futureTransactions: Observable<SignedTransaction>) = proxy.verifiedTransactions()
|
||||||
|
// END 3
|
||||||
|
|
||||||
|
// START 4
|
||||||
|
when (printOrVisualise) {
|
||||||
|
PrintOrVisualise.Print -> {
|
||||||
|
futureTransactions.startWith(transactions).subscribe { transaction ->
|
||||||
|
println("NODE ${transaction.id}")
|
||||||
|
transaction.tx.inputs.forEach { input ->
|
||||||
|
println("EDGE ${input.txhash} ${transaction.id}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
futureTransactions.subscribe { transaction ->
|
// END 4
|
||||||
graph.addNode<Node>("${transaction.id}")
|
// START 5
|
||||||
transaction.tx.inputs.forEach { ref ->
|
PrintOrVisualise.Visualise -> {
|
||||||
graph.addEdge<Edge>("$ref", "${ref.txhash}", "${transaction.id}")
|
val graph = MultiGraph("transactions")
|
||||||
|
transactions.forEach { transaction ->
|
||||||
|
graph.addNode<Node>("${transaction.id}")
|
||||||
}
|
}
|
||||||
|
transactions.forEach { transaction ->
|
||||||
|
transaction.tx.inputs.forEach { ref ->
|
||||||
|
graph.addEdge<Edge>("$ref", "${ref.txhash}", "${transaction.id}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
futureTransactions.subscribe { transaction ->
|
||||||
|
graph.addNode<Node>("${transaction.id}")
|
||||||
|
transaction.tx.inputs.forEach { ref ->
|
||||||
|
graph.addEdge<Edge>("$ref", "${ref.txhash}", "${transaction.id}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
graph.display()
|
||||||
}
|
}
|
||||||
graph.display()
|
}
|
||||||
|
waitForAllNodesToFinish()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// END 5
|
||||||
|
|
||||||
|
// START 6
|
||||||
|
fun generateTransactions(proxy: CordaRPCOps) {
|
||||||
|
var ownedQuantity = proxy.vaultAndUpdates().first.fold(0L) { sum, state ->
|
||||||
|
sum + (state.state.data as Cash.State).amount.quantity
|
||||||
|
}
|
||||||
|
val issueRef = OpaqueBytes.of(0)
|
||||||
|
val notary = proxy.networkMapUpdates().first.first { it.advertisedServices.any { it.info.type.isNotary() } }.notaryIdentity
|
||||||
|
val me = proxy.nodeIdentity().legalIdentity
|
||||||
|
val meAndRef = PartyAndReference(me, issueRef)
|
||||||
|
while (true) {
|
||||||
|
Thread.sleep(1000)
|
||||||
|
val random = SplittableRandom()
|
||||||
|
val n = random.nextDouble()
|
||||||
|
if (ownedQuantity > 10000 && n > 0.8) {
|
||||||
|
val quantity = Math.abs(random.nextLong()) % 2000
|
||||||
|
proxy.startProtocol(::CashProtocol, CashCommand.ExitCash(Amount(quantity, USD), issueRef))
|
||||||
|
ownedQuantity -= quantity
|
||||||
|
} else if (ownedQuantity > 1000 && n < 0.7) {
|
||||||
|
val quantity = Math.abs(random.nextLong() % Math.min(ownedQuantity, 2000))
|
||||||
|
proxy.startProtocol(::CashProtocol, CashCommand.PayCash(Amount(quantity, Issued(meAndRef, USD)), me))
|
||||||
|
} else {
|
||||||
|
val quantity = Math.abs(random.nextLong() % 1000)
|
||||||
|
proxy.startProtocol(::CashProtocol, CashCommand.IssueCash(Amount(quantity, USD), issueRef, me, notary))
|
||||||
|
ownedQuantity += quantity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// END 5
|
// END 6
|
@ -1,31 +1,33 @@
|
|||||||
.. _graphstream: http://graphstream-project.org/
|
.. _graphstream: http://graphstream-project.org/
|
||||||
|
|
||||||
Client RPC API
|
Client RPC API Tutorial
|
||||||
==============
|
=======================
|
||||||
|
|
||||||
In this tutorial we will build a simple command line utility that connects to a node and dumps the transaction graph to
|
In this tutorial we will build a simple command line utility that
|
||||||
the standard output. We will then put some simple visualisation on top. For an explanation on how the RPC works see
|
connects to a node, creates some Cash transactions and meanwhile dumps
|
||||||
:doc:`clientrpc`.
|
the transaction graph to the standard output. We will then put some
|
||||||
|
simple visualisation on top. For an explanation on how the RPC works
|
||||||
|
see :doc:`clientrpc`.
|
||||||
|
|
||||||
We start off by connecting to the node itself. For the purposes of the tutorial we will run the Trader demo on some
|
We start off by connecting to the node itself. For the purposes of the tutorial we will use the Driver to start up a notary and a node that issues/exits and moves Cash around for herself. To authenticate we will use the certificates of the nodes directly.
|
||||||
local port and connect to the Buyer side. We will pass in the address as a command line argument. To connect to the node
|
|
||||||
we also need to access the certificates of the node, we will access the node's ``certificates`` directory directly.
|
|
||||||
|
|
||||||
.. literalinclude:: example-code/src/main/kotlin/net.corda.docs/ClientRpcTutorial.kt
|
Note how we configure the node to create a user that has permission to start the CashProtocol.
|
||||||
|
|
||||||
|
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt
|
||||||
:language: kotlin
|
:language: kotlin
|
||||||
:start-after: START 1
|
:start-after: START 1
|
||||||
:end-before: END 1
|
:end-before: END 1
|
||||||
|
|
||||||
Now we can connect to the node itself using a valid RPC login. By default the user `user1` is available with password `test`.
|
Now we can connect to the node itself using a valid RPC login. We login using the configured user.
|
||||||
|
|
||||||
.. literalinclude:: example-code/src/main/kotlin/net.corda.docs/ClientRpcTutorial.kt
|
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt
|
||||||
:language: kotlin
|
:language: kotlin
|
||||||
:start-after: START 2
|
:start-after: START 2
|
||||||
:end-before: END 2
|
:end-before: END 2
|
||||||
|
|
||||||
``proxy`` now exposes the full RPC interface of the node:
|
We start generating transactions in a different thread (``generateTransactions`` to be defined later) using ``proxy``, which exposes the full RPC interface of the node:
|
||||||
|
|
||||||
.. literalinclude:: ../../node/src/main/kotlin/net.corda.node/services/messaging/CordaRPCOps.kt
|
.. literalinclude:: ../../node/src/main/kotlin/net/corda/node/services/messaging/CordaRPCOps.kt
|
||||||
:language: kotlin
|
:language: kotlin
|
||||||
:start-after: interface CordaRPCOps
|
:start-after: interface CordaRPCOps
|
||||||
:end-before: }
|
:end-before: }
|
||||||
@ -34,7 +36,7 @@ The one we need in order to dump the transaction graph is ``verifiedTransactions
|
|||||||
RPC will return a list of transactions and an Observable stream. This is a general pattern, we query some data and the
|
RPC will return a list of transactions and an Observable stream. This is a general pattern, we query some data and the
|
||||||
node will return the current snapshot and future updates done to it.
|
node will return the current snapshot and future updates done to it.
|
||||||
|
|
||||||
.. literalinclude:: example-code/src/main/kotlin/net.corda.docs/ClientRpcTutorial.kt
|
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt
|
||||||
:language: kotlin
|
:language: kotlin
|
||||||
:start-after: START 3
|
:start-after: START 3
|
||||||
:end-before: END 3
|
:end-before: END 3
|
||||||
@ -43,41 +45,37 @@ The graph will be defined by nodes and edges between them. Each node represents
|
|||||||
output-input relations. For now let's just print ``NODE <txhash>`` for the former and ``EDGE <txhash> <txhash>`` for the
|
output-input relations. For now let's just print ``NODE <txhash>`` for the former and ``EDGE <txhash> <txhash>`` for the
|
||||||
latter.
|
latter.
|
||||||
|
|
||||||
.. literalinclude:: example-code/src/main/kotlin/net.corda.docs/ClientRpcTutorial.kt
|
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt
|
||||||
:language: kotlin
|
:language: kotlin
|
||||||
:start-after: START 4
|
:start-after: START 4
|
||||||
:end-before: END 4
|
:end-before: END 4
|
||||||
|
|
||||||
Now we can start the trader demo as per described in :doc:`running-the-demos`::
|
|
||||||
|
|
||||||
# Build the demo
|
Now we just need to create the transactions themselves!
|
||||||
./gradlew installDist
|
|
||||||
# Start the buyer
|
|
||||||
./build/install/r3prototyping/bin/trader-demo --role=BUYER
|
|
||||||
|
|
||||||
In another terminal we can connect to it with our client::
|
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt
|
||||||
|
:language: kotlin
|
||||||
|
:start-after: START 6
|
||||||
|
:end-before: END 6
|
||||||
|
|
||||||
# Connect to localhost:31337
|
We utilise several RPC functions here to query things like the notaries in the node cluster or our own vault.
|
||||||
./docs/source/example-code/build/install/docs/source/example-code/bin/client-rpc-tutorial localhost:31337 Print
|
|
||||||
|
|
||||||
We should see some ``NODE``-s printed. This is because the buyer self-issues some cash for the demo.
|
Then in a loop we generate randomly either an Issue, a Pay or an Exit transaction.
|
||||||
Unless we ran the seller before we shouldn't see any ``EDGE``-s because the cash hasn't been spent yet.
|
|
||||||
|
|
||||||
In another terminal we can now start the seller::
|
The RPC we need to initiate a Cash transaction is ``startProtocolDynamic`` which may start an arbitrary protocol, given sufficient permissions to do so. We won't use this function directly, but rather a type-safe wrapper around it ``startProtocol`` that type-checks the arguments for us.
|
||||||
|
|
||||||
# Start sellers in a loop
|
Finally we have everything in place: we start a couple of nodes, connect to them, and start creating transactions while listening on successfully created ones, which are dumped to the console. We just need to run it!:
|
||||||
for i in {0..9} ; do ./build/install/r3prototyping/bin/trader-demo --role=SELLER ; done
|
|
||||||
|
|
||||||
We should start seeing new ``NODE``-s and ``EDGE``-s appearing.
|
# Build the example
|
||||||
|
./gradlew docs/source/example-code:installDist
|
||||||
|
# Start it
|
||||||
|
./docs/source/example-code/build/install/docs/source/example-code/bin/client-rpc-tutorial Print
|
||||||
|
|
||||||
Now let's try to visualise the transaction graph. We will use a graph drawing library called graphstream_
|
Now let's try to visualise the transaction graph. We will use a graph drawing library called graphstream_
|
||||||
|
|
||||||
.. literalinclude:: example-code/src/main/kotlin/net.corda.docs/ClientRpcTutorial.kt
|
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt
|
||||||
:language: kotlin
|
:language: kotlin
|
||||||
:start-after: START 5
|
:start-after: START 5
|
||||||
:end-before: END 5
|
:end-before: END 5
|
||||||
|
|
||||||
If we run the client with ``Visualise`` we should see a simple graph being drawn as new transactions are being created
|
If we run the client with ``Visualise`` we should see a simple random graph being drawn as new transactions are being created.
|
||||||
by the seller runs.
|
|
||||||
|
|
||||||
That's it! We saw how to connect to the node and stream data from it.
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
apply plugin: 'kotlin'
|
apply plugin: 'kotlin'
|
||||||
apply plugin: CanonicalizerPlugin
|
apply plugin: CanonicalizerPlugin
|
||||||
apply plugin: DefaultPublishTasks
|
apply plugin: DefaultPublishTasks
|
||||||
|
apply plugin: QuasarPlugin
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenLocal()
|
mavenLocal()
|
||||||
|
168
finance/src/main/kotlin/net/corda/protocols/CashProtocol.kt
Normal file
168
finance/src/main/kotlin/net/corda/protocols/CashProtocol.kt
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
package net.corda.protocols
|
||||||
|
|
||||||
|
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.crypto.keys
|
||||||
|
import net.corda.core.crypto.toStringShort
|
||||||
|
import net.corda.core.protocols.ProtocolLogic
|
||||||
|
import net.corda.core.protocols.StateMachineRunId
|
||||||
|
import net.corda.core.serialization.OpaqueBytes
|
||||||
|
import net.corda.core.transactions.SignedTransaction
|
||||||
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
|
import java.security.KeyPair
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiates a protocol that produces an Issue/Move or Exit Cash transaction.
|
||||||
|
*
|
||||||
|
* @param command Indicates what Cash transaction to create with what parameters.
|
||||||
|
*/
|
||||||
|
class CashProtocol(val command: CashCommand): ProtocolLogic<CashProtocolResult>() {
|
||||||
|
|
||||||
|
@Suspendable
|
||||||
|
override fun call(): CashProtocolResult {
|
||||||
|
return when (command) {
|
||||||
|
is CashCommand.IssueCash -> issueCash(command)
|
||||||
|
is CashCommand.PayCash -> initiatePayment(command)
|
||||||
|
is CashCommand.ExitCash -> exitCash(command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO check with the recipient if they want to accept the cash.
|
||||||
|
@Suspendable
|
||||||
|
private fun initiatePayment(req: CashCommand.PayCash): CashProtocolResult {
|
||||||
|
val builder: TransactionBuilder = TransactionType.General.Builder(null)
|
||||||
|
// TODO: Have some way of restricting this to states the caller controls
|
||||||
|
try {
|
||||||
|
val (spendTX, keysForSigning) = serviceHub.vaultService.generateSpend(builder,
|
||||||
|
req.amount.withoutIssuer(), req.recipient.owningKey, setOf(req.amount.token.issuer.party))
|
||||||
|
|
||||||
|
keysForSigning.keys.forEach {
|
||||||
|
val key = serviceHub.keyManagementService.keys[it] ?: throw IllegalStateException("Could not find signing key for ${it.toStringShort()}")
|
||||||
|
builder.signWith(KeyPair(it, key))
|
||||||
|
}
|
||||||
|
|
||||||
|
val tx = spendTX.toSignedTransaction(checkSufficientSignatures = false)
|
||||||
|
val protocol = FinalityProtocol(tx, setOf(req.recipient))
|
||||||
|
subProtocol(protocol)
|
||||||
|
return CashProtocolResult.Success(
|
||||||
|
psm.id,
|
||||||
|
tx,
|
||||||
|
"Cash payment transaction generated"
|
||||||
|
)
|
||||||
|
} catch(ex: InsufficientBalanceException) {
|
||||||
|
return CashProtocolResult.Failed(ex.message ?: "Insufficient balance")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suspendable
|
||||||
|
private fun exitCash(req: CashCommand.ExitCash): CashProtocolResult {
|
||||||
|
val builder: TransactionBuilder = TransactionType.General.Builder(null)
|
||||||
|
try {
|
||||||
|
val issuer = PartyAndReference(serviceHub.myInfo.legalIdentity, req.issueRef)
|
||||||
|
Cash().generateExit(builder, req.amount.issuedBy(issuer),
|
||||||
|
serviceHub.vaultService.currentVault.statesOfType<Cash.State>().filter { it.state.data.owner == issuer.party.owningKey })
|
||||||
|
val myKey = serviceHub.legalIdentityKey
|
||||||
|
builder.signWith(myKey)
|
||||||
|
|
||||||
|
// Work out who the owners of the burnt states were
|
||||||
|
val inputStatesNullable = serviceHub.vaultService.statesForRefs(builder.inputStates())
|
||||||
|
val inputStates = inputStatesNullable.values.filterNotNull().map { it.data }
|
||||||
|
if (inputStatesNullable.size != inputStates.size) {
|
||||||
|
val unresolvedStateRefs = inputStatesNullable.filter { it.value == null }.map { it.key }
|
||||||
|
throw InputStateRefResolveFailed(unresolvedStateRefs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Is it safe to drop participants we don't know how to contact? Does not knowing how to contact them
|
||||||
|
// count as a reason to fail?
|
||||||
|
val participants: Set<Party> = inputStates.filterIsInstance<Cash.State>().map { serviceHub.identityService.partyFromKey(it.owner) }.filterNotNull().toSet()
|
||||||
|
|
||||||
|
// Commit the transaction
|
||||||
|
val tx = builder.toSignedTransaction(checkSufficientSignatures = false)
|
||||||
|
subProtocol(FinalityProtocol(tx, participants))
|
||||||
|
return CashProtocolResult.Success(
|
||||||
|
psm.id,
|
||||||
|
tx,
|
||||||
|
"Cash destruction transaction generated"
|
||||||
|
)
|
||||||
|
} catch (ex: InsufficientBalanceException) {
|
||||||
|
return CashProtocolResult.Failed(ex.message ?: "Insufficient balance")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suspendable
|
||||||
|
private fun issueCash(req: CashCommand.IssueCash): CashProtocolResult {
|
||||||
|
val builder: TransactionBuilder = TransactionType.General.Builder(notary = null)
|
||||||
|
val issuer = PartyAndReference(serviceHub.myInfo.legalIdentity, req.issueRef)
|
||||||
|
Cash().generateIssue(builder, req.amount.issuedBy(issuer), req.recipient.owningKey, req.notary)
|
||||||
|
val myKey = serviceHub.legalIdentityKey
|
||||||
|
builder.signWith(myKey)
|
||||||
|
val tx = builder.toSignedTransaction(checkSufficientSignatures = true)
|
||||||
|
// Issuance transactions do not need to be notarised, so we can skip directly to broadcasting it
|
||||||
|
subProtocol(BroadcastTransactionProtocol(tx, setOf(req.recipient)))
|
||||||
|
return CashProtocolResult.Success(
|
||||||
|
psm.id,
|
||||||
|
tx,
|
||||||
|
"Cash issuance completed"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A command to initiate the Cash protocol with.
|
||||||
|
*/
|
||||||
|
sealed class CashCommand {
|
||||||
|
/**
|
||||||
|
* Issue cash state objects.
|
||||||
|
*
|
||||||
|
* @param amount the amount of currency to issue on to the ledger.
|
||||||
|
* @param issueRef the reference to specify on the issuance, used to differentiate pools of cash. Convention is
|
||||||
|
* to use the single byte "0x01" as a default.
|
||||||
|
* @param recipient the party to issue the cash to.
|
||||||
|
* @param notary the notary to use for this transaction.
|
||||||
|
*/
|
||||||
|
class IssueCash(val amount: Amount<Currency>,
|
||||||
|
val issueRef: OpaqueBytes,
|
||||||
|
val recipient: Party,
|
||||||
|
val notary: Party) : CashCommand()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pay cash to someone else.
|
||||||
|
*
|
||||||
|
* @param amount the amount of currency to issue on to the ledger.
|
||||||
|
* @param recipient the party to issue the cash to.
|
||||||
|
*/
|
||||||
|
class PayCash(val amount: Amount<Issued<Currency>>, val recipient: Party) : CashCommand()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exit cash from the ledger.
|
||||||
|
*
|
||||||
|
* @param amount the amount of currency to exit from the ledger.
|
||||||
|
* @param issueRef the reference previously specified on the issuance.
|
||||||
|
*/
|
||||||
|
class ExitCash(val amount: Amount<Currency>, val issueRef: OpaqueBytes) : CashCommand()
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class CashProtocolResult {
|
||||||
|
/**
|
||||||
|
* @param transaction the transaction created as a result, in the case where the protocol completed successfully.
|
||||||
|
*/
|
||||||
|
class Success(val id: StateMachineRunId, val transaction: SignedTransaction?, val message: String?) : CashProtocolResult() {
|
||||||
|
override fun toString() = "Success($message)"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State indicating the action undertaken failed, either directly (it is not something which requires a
|
||||||
|
* state machine), or before a state machine was started.
|
||||||
|
*/
|
||||||
|
class Failed(val message: String?) : CashProtocolResult() {
|
||||||
|
override fun toString() = "Failed($message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InputStateRefResolveFailed(stateRefs: List<StateRef>) :
|
||||||
|
Exception("Failed to resolve input StateRefs $stateRefs")
|
@ -546,6 +546,7 @@ class CashTests {
|
|||||||
|
|
||||||
val wtx = makeSpend(100.DOLLARS, THEIR_PUBKEY_1)
|
val wtx = makeSpend(100.DOLLARS, THEIR_PUBKEY_1)
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
val vaultState = vaultService.states.elementAt(0) as StateAndRef<Cash.State>
|
val vaultState = vaultService.states.elementAt(0) as StateAndRef<Cash.State>
|
||||||
assertEquals(vaultState.ref, wtx.inputs[0])
|
assertEquals(vaultState.ref, wtx.inputs[0])
|
||||||
assertEquals(vaultState.state.data.copy(owner = THEIR_PUBKEY_1), wtx.outputs[0].data)
|
assertEquals(vaultState.state.data.copy(owner = THEIR_PUBKEY_1), wtx.outputs[0].data)
|
||||||
@ -572,6 +573,7 @@ class CashTests {
|
|||||||
|
|
||||||
val wtx = makeSpend(10.DOLLARS, THEIR_PUBKEY_1)
|
val wtx = makeSpend(10.DOLLARS, THEIR_PUBKEY_1)
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
val vaultState = vaultService.states.elementAt(0) as StateAndRef<Cash.State>
|
val vaultState = vaultService.states.elementAt(0) as StateAndRef<Cash.State>
|
||||||
assertEquals(vaultState.ref, wtx.inputs[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(owner = THEIR_PUBKEY_1, amount = 10.DOLLARS `issued by` defaultIssuer), wtx.outputs[0].data)
|
||||||
@ -586,6 +588,7 @@ class CashTests {
|
|||||||
databaseTransaction(database) {
|
databaseTransaction(database) {
|
||||||
val wtx = makeSpend(500.DOLLARS, THEIR_PUBKEY_1)
|
val wtx = makeSpend(500.DOLLARS, THEIR_PUBKEY_1)
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
val vaultState0 = vaultService.states.elementAt(0) as StateAndRef<Cash.State>
|
val vaultState0 = vaultService.states.elementAt(0) as StateAndRef<Cash.State>
|
||||||
val vaultState1 = vaultService.states.elementAt(1)
|
val vaultState1 = vaultService.states.elementAt(1)
|
||||||
assertEquals(vaultState0.ref, wtx.inputs[0])
|
assertEquals(vaultState0.ref, wtx.inputs[0])
|
||||||
@ -602,8 +605,10 @@ class CashTests {
|
|||||||
val wtx = makeSpend(580.DOLLARS, THEIR_PUBKEY_1)
|
val wtx = makeSpend(580.DOLLARS, THEIR_PUBKEY_1)
|
||||||
assertEquals(3, wtx.inputs.size)
|
assertEquals(3, wtx.inputs.size)
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
val vaultState0 = vaultService.states.elementAt(0) as StateAndRef<Cash.State>
|
val vaultState0 = vaultService.states.elementAt(0) as StateAndRef<Cash.State>
|
||||||
val vaultState1 = vaultService.states.elementAt(1)
|
val vaultState1 = vaultService.states.elementAt(1)
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
val vaultState2 = vaultService.states.elementAt(2) as StateAndRef<Cash.State>
|
val vaultState2 = vaultService.states.elementAt(2) as StateAndRef<Cash.State>
|
||||||
assertEquals(vaultState0.ref, wtx.inputs[0])
|
assertEquals(vaultState0.ref, wtx.inputs[0])
|
||||||
assertEquals(vaultState1.ref, wtx.inputs[1])
|
assertEquals(vaultState1.ref, wtx.inputs[1])
|
||||||
|
@ -72,7 +72,7 @@ class APIServerImpl(val node: AbstractNode) : APIServer {
|
|||||||
if (type is ProtocolClassRef) {
|
if (type is ProtocolClassRef) {
|
||||||
val protocolLogicRef = node.services.protocolLogicRefFactory.createKotlin(type.className, args)
|
val protocolLogicRef = node.services.protocolLogicRefFactory.createKotlin(type.className, args)
|
||||||
val protocolInstance = node.services.protocolLogicRefFactory.toProtocolLogic(protocolLogicRef)
|
val protocolInstance = node.services.protocolLogicRefFactory.toProtocolLogic(protocolLogicRef)
|
||||||
return node.services.startProtocol(protocolInstance)
|
return node.services.startProtocol(protocolInstance).resultFuture
|
||||||
} else {
|
} else {
|
||||||
throw UnsupportedOperationException("Unsupported ProtocolRef type: $type")
|
throw UnsupportedOperationException("Unsupported ProtocolRef type: $type")
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ import net.corda.core.node.services.*
|
|||||||
import net.corda.core.node.services.NetworkMapCache.MapChangeType
|
import net.corda.core.node.services.NetworkMapCache.MapChangeType
|
||||||
import net.corda.core.protocols.ProtocolLogic
|
import net.corda.core.protocols.ProtocolLogic
|
||||||
import net.corda.core.protocols.ProtocolLogicRefFactory
|
import net.corda.core.protocols.ProtocolLogicRefFactory
|
||||||
|
import net.corda.core.protocols.ProtocolStateMachine
|
||||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||||
import net.corda.core.serialization.deserialize
|
import net.corda.core.serialization.deserialize
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
@ -43,6 +44,8 @@ import net.corda.node.services.transactions.ValidatingNotaryService
|
|||||||
import net.corda.node.services.vault.CashBalanceAsMetricsObserver
|
import net.corda.node.services.vault.CashBalanceAsMetricsObserver
|
||||||
import net.corda.node.services.vault.NodeVaultService
|
import net.corda.node.services.vault.NodeVaultService
|
||||||
import net.corda.node.utilities.*
|
import net.corda.node.utilities.*
|
||||||
|
import net.corda.protocols.CashCommand
|
||||||
|
import net.corda.protocols.CashProtocol
|
||||||
import net.corda.protocols.sendRequest
|
import net.corda.protocols.sendRequest
|
||||||
import org.jetbrains.exposed.sql.Database
|
import org.jetbrains.exposed.sql.Database
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
@ -110,7 +113,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val netwo
|
|||||||
override val monitoringService: MonitoringService = MonitoringService(MetricRegistry())
|
override val monitoringService: MonitoringService = MonitoringService(MetricRegistry())
|
||||||
override val protocolLogicRefFactory: ProtocolLogicRefFactory get() = protocolLogicFactory
|
override val protocolLogicRefFactory: ProtocolLogicRefFactory get() = protocolLogicFactory
|
||||||
|
|
||||||
override fun <T> startProtocol(logic: ProtocolLogic<T>): ListenableFuture<T> = smm.add(logic).resultFuture
|
override fun <T> startProtocol(logic: ProtocolLogic<T>): ProtocolStateMachine<T> = smm.add(logic)
|
||||||
|
|
||||||
override fun registerProtocolInitiator(markerClass: KClass<*>, protocolFactory: (Party) -> ProtocolLogic<*>) {
|
override fun registerProtocolInitiator(markerClass: KClass<*>, protocolFactory: (Party) -> ProtocolLogic<*>) {
|
||||||
require(markerClass !in protocolFactories) { "${markerClass.java.name} has already been used to register a protocol" }
|
require(markerClass !in protocolFactories) { "${markerClass.java.name} has already been used to register a protocol" }
|
||||||
@ -307,8 +310,24 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val netwo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val defaultProtocolWhiteList: Map<Class<out ProtocolLogic<*>>, Set<Class<*>>> = mapOf(
|
||||||
|
CashProtocol::class.java to setOf(
|
||||||
|
CashCommand.IssueCash::class.java,
|
||||||
|
CashCommand.PayCash::class.java,
|
||||||
|
CashCommand.ExitCash::class.java
|
||||||
|
)
|
||||||
|
)
|
||||||
private fun initialiseProtocolLogicFactory(): ProtocolLogicRefFactory {
|
private fun initialiseProtocolLogicFactory(): ProtocolLogicRefFactory {
|
||||||
val protocolWhitelist = HashMap<String, Set<String>>()
|
val protocolWhitelist = HashMap<String, Set<String>>()
|
||||||
|
|
||||||
|
for ((protocolClass, extraArgumentTypes) in defaultProtocolWhiteList) {
|
||||||
|
val argumentWhitelistClassNames = HashSet(extraArgumentTypes.map { it.name })
|
||||||
|
protocolClass.constructors.forEach {
|
||||||
|
it.parameters.mapTo(argumentWhitelistClassNames) { it.type.name }
|
||||||
|
}
|
||||||
|
protocolWhitelist.merge(protocolClass.name, argumentWhitelistClassNames, { x, y -> x + y })
|
||||||
|
}
|
||||||
|
|
||||||
for (plugin in pluginRegistries) {
|
for (plugin in pluginRegistries) {
|
||||||
for ((className, classWhitelist) in plugin.requiredProtocols) {
|
for ((className, classWhitelist) in plugin.requiredProtocols) {
|
||||||
protocolWhitelist.merge(className, classWhitelist, { x, y -> x + y })
|
protocolWhitelist.merge(className, classWhitelist, { x, y -> x + y })
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
package net.corda.node.internal
|
package net.corda.node.internal
|
||||||
|
|
||||||
import net.corda.contracts.asset.Cash
|
import net.corda.core.contracts.ContractState
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.StateAndRef
|
||||||
import net.corda.core.crypto.Party
|
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.crypto.keys
|
import net.corda.core.crypto.keys
|
||||||
import net.corda.core.crypto.toStringShort
|
import net.corda.core.crypto.toStringShort
|
||||||
@ -11,16 +10,17 @@ import net.corda.core.node.ServiceHub
|
|||||||
import net.corda.core.node.services.NetworkMapCache
|
import net.corda.core.node.services.NetworkMapCache
|
||||||
import net.corda.core.node.services.StateMachineTransactionMapping
|
import net.corda.core.node.services.StateMachineTransactionMapping
|
||||||
import net.corda.core.node.services.Vault
|
import net.corda.core.node.services.Vault
|
||||||
|
import net.corda.core.protocols.ProtocolLogic
|
||||||
|
import net.corda.core.toObservable
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.utilities.ProgressTracker
|
||||||
import net.corda.node.services.messaging.*
|
import net.corda.node.services.messaging.*
|
||||||
|
import net.corda.node.services.startProtocolPermission
|
||||||
|
import net.corda.node.services.statemachine.ProtocolStateMachineImpl
|
||||||
import net.corda.node.services.statemachine.StateMachineManager
|
import net.corda.node.services.statemachine.StateMachineManager
|
||||||
import net.corda.node.utilities.databaseTransaction
|
import net.corda.node.utilities.databaseTransaction
|
||||||
import net.corda.protocols.BroadcastTransactionProtocol
|
|
||||||
import net.corda.protocols.FinalityProtocol
|
|
||||||
import org.jetbrains.exposed.sql.Database
|
import org.jetbrains.exposed.sql.Database
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import java.security.KeyPair
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Server side implementations of RPCs available to MQ based client tools. Execution takes place on the server
|
* Server side implementations of RPCs available to MQ based client tools. Execution takes place on the server
|
||||||
@ -31,10 +31,6 @@ class CordaRPCOpsImpl(
|
|||||||
val smm: StateMachineManager,
|
val smm: StateMachineManager,
|
||||||
val database: Database
|
val database: Database
|
||||||
) : CordaRPCOps {
|
) : CordaRPCOps {
|
||||||
companion object {
|
|
||||||
const val CASH_PERMISSION = "CASH"
|
|
||||||
}
|
|
||||||
|
|
||||||
override val protocolVersion: Int get() = 0
|
override val protocolVersion: Int get() = 0
|
||||||
|
|
||||||
override fun networkMapUpdates(): Pair<List<NodeInfo>, Observable<NetworkMapCache.MapChange>> {
|
override fun networkMapUpdates(): Pair<List<NodeInfo>, Observable<NetworkMapCache.MapChange>> {
|
||||||
@ -67,17 +63,6 @@ class CordaRPCOpsImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun executeCommand(command: ClientToServiceCommand): TransactionBuildResult {
|
|
||||||
requirePermission(CASH_PERMISSION)
|
|
||||||
return databaseTransaction(database) {
|
|
||||||
when (command) {
|
|
||||||
is ClientToServiceCommand.IssueCash -> issueCash(command)
|
|
||||||
is ClientToServiceCommand.PayCash -> initiatePayment(command)
|
|
||||||
is ClientToServiceCommand.ExitCash -> exitCash(command)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun nodeIdentity(): NodeInfo {
|
override fun nodeIdentity(): NodeInfo {
|
||||||
return services.myInfo
|
return services.myInfo
|
||||||
}
|
}
|
||||||
@ -94,83 +79,14 @@ class CordaRPCOpsImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Make a lightweight protocol that manages this workflow, rather than embedding it directly in the service
|
// TODO: Check that this protocol is annotated as being intended for RPC invocation
|
||||||
private fun initiatePayment(req: ClientToServiceCommand.PayCash): TransactionBuildResult {
|
override fun <T: Any> startProtocolDynamic(logicType: Class<out ProtocolLogic<T>>, vararg args: Any?): ProtocolHandle<T> {
|
||||||
val builder: TransactionBuilder = TransactionType.General.Builder(null)
|
requirePermission(startProtocolPermission(logicType))
|
||||||
// TODO: Have some way of restricting this to states the caller controls
|
val stateMachine = services.invokeProtocolAsync(logicType, *args) as ProtocolStateMachineImpl<T>
|
||||||
try {
|
return ProtocolHandle(
|
||||||
val (spendTX, keysForSigning) = services.vaultService.generateSpend(builder,
|
id = stateMachine.id,
|
||||||
req.amount.withoutIssuer(), req.recipient.owningKey, setOf(req.amount.token.issuer.party))
|
progress = stateMachine.logic.progressTracker?.changes ?: Observable.empty<ProgressTracker.Change>(),
|
||||||
|
returnValue = stateMachine.resultFuture.toObservable()
|
||||||
keysForSigning.keys.forEach {
|
|
||||||
val key = services.keyManagementService.keys[it] ?: throw IllegalStateException("Could not find signing key for ${it.toStringShort()}")
|
|
||||||
builder.signWith(KeyPair(it, key))
|
|
||||||
}
|
|
||||||
|
|
||||||
val tx = spendTX.toSignedTransaction(checkSufficientSignatures = false)
|
|
||||||
val protocol = FinalityProtocol(tx, setOf(req), setOf(req.recipient))
|
|
||||||
return TransactionBuildResult.ProtocolStarted(
|
|
||||||
smm.add(protocol).id,
|
|
||||||
tx,
|
|
||||||
"Cash payment transaction generated"
|
|
||||||
)
|
|
||||||
} catch(ex: InsufficientBalanceException) {
|
|
||||||
return TransactionBuildResult.Failed(ex.message ?: "Insufficient balance")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Make a lightweight protocol that manages this workflow, rather than embedding it directly in the service
|
|
||||||
private fun exitCash(req: ClientToServiceCommand.ExitCash): TransactionBuildResult {
|
|
||||||
val builder: TransactionBuilder = TransactionType.General.Builder(null)
|
|
||||||
try {
|
|
||||||
val issuer = PartyAndReference(services.myInfo.legalIdentity, req.issueRef)
|
|
||||||
Cash().generateExit(builder, req.amount.issuedBy(issuer),
|
|
||||||
services.vaultService.currentVault.statesOfType<Cash.State>().filter { it.state.data.owner == issuer.party.owningKey })
|
|
||||||
val myKey = services.legalIdentityKey
|
|
||||||
builder.signWith(myKey)
|
|
||||||
|
|
||||||
// Work out who the owners of the burnt states were
|
|
||||||
val inputStatesNullable = services.vaultService.statesForRefs(builder.inputStates())
|
|
||||||
val inputStates = inputStatesNullable.values.filterNotNull().map { it.data }
|
|
||||||
if (inputStatesNullable.size != inputStates.size) {
|
|
||||||
val unresolvedStateRefs = inputStatesNullable.filter { it.value == null }.map { it.key }
|
|
||||||
throw InputStateRefResolveFailed(unresolvedStateRefs)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Is it safe to drop participants we don't know how to contact? Does not knowing how to contact them
|
|
||||||
// count as a reason to fail?
|
|
||||||
val participants: Set<Party> = inputStates.filterIsInstance<Cash.State>().map { services.identityService.partyFromKey(it.owner) }.filterNotNull().toSet()
|
|
||||||
|
|
||||||
// Commit the transaction
|
|
||||||
val tx = builder.toSignedTransaction(checkSufficientSignatures = false)
|
|
||||||
val protocol = FinalityProtocol(tx, setOf(req), participants)
|
|
||||||
return TransactionBuildResult.ProtocolStarted(
|
|
||||||
smm.add(protocol).id,
|
|
||||||
tx,
|
|
||||||
"Cash destruction transaction generated"
|
|
||||||
)
|
|
||||||
} catch (ex: InsufficientBalanceException) {
|
|
||||||
return TransactionBuildResult.Failed(ex.message ?: "Insufficient balance")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Make a lightweight protocol that manages this workflow, rather than embedding it directly in the service
|
|
||||||
private fun issueCash(req: ClientToServiceCommand.IssueCash): TransactionBuildResult {
|
|
||||||
val builder: TransactionBuilder = TransactionType.General.Builder(notary = null)
|
|
||||||
val issuer = PartyAndReference(services.myInfo.legalIdentity, req.issueRef)
|
|
||||||
Cash().generateIssue(builder, req.amount.issuedBy(issuer), req.recipient.owningKey, req.notary)
|
|
||||||
val myKey = services.legalIdentityKey
|
|
||||||
builder.signWith(myKey)
|
|
||||||
val tx = builder.toSignedTransaction(checkSufficientSignatures = true)
|
|
||||||
// Issuance transactions do not need to be notarised, so we can skip directly to broadcasting it
|
|
||||||
val protocol = BroadcastTransactionProtocol(tx, setOf(req), setOf(req.recipient))
|
|
||||||
return TransactionBuildResult.ProtocolStarted(
|
|
||||||
smm.add(protocol).id,
|
|
||||||
tx,
|
|
||||||
"Cash issuance completed"
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
class InputStateRefResolveFailed(stateRefs: List<StateRef>) :
|
|
||||||
Exception("Failed to resolve input StateRefs $stateRefs")
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package net.corda.node.services
|
package net.corda.node.services
|
||||||
|
|
||||||
import com.typesafe.config.Config
|
import com.typesafe.config.Config
|
||||||
|
import net.corda.core.protocols.ProtocolLogic
|
||||||
import net.corda.node.services.config.getListOrElse
|
import net.corda.node.services.config.getListOrElse
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -9,7 +10,7 @@ import net.corda.node.services.config.getListOrElse
|
|||||||
* to. These permissions are represented as [String]s to allow RPC implementations to add their own permissioning.
|
* to. These permissions are represented as [String]s to allow RPC implementations to add their own permissioning.
|
||||||
*/
|
*/
|
||||||
interface RPCUserService {
|
interface RPCUserService {
|
||||||
fun getUser(usename: String): User?
|
fun getUser(username: String): User?
|
||||||
val users: List<User>
|
val users: List<User>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,13 +26,13 @@ class RPCUserServiceImpl(config: Config) : RPCUserService {
|
|||||||
val username = it.getString("user")
|
val username = it.getString("user")
|
||||||
require(username.matches("\\w+".toRegex())) { "Username $username contains invalid characters" }
|
require(username.matches("\\w+".toRegex())) { "Username $username contains invalid characters" }
|
||||||
val password = it.getString("password")
|
val password = it.getString("password")
|
||||||
val permissions = it.getListOrElse<String>("permissions") { emptyList() }.map(String::toUpperCase).toSet()
|
val permissions = it.getListOrElse<String>("permissions") { emptyList() }.toSet()
|
||||||
User(username, password, permissions)
|
User(username, password, permissions)
|
||||||
}
|
}
|
||||||
.associateBy(User::username)
|
.associateBy(User::username)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getUser(usename: String): User? = _users[usename]
|
override fun getUser(username: String): User? = _users[username]
|
||||||
|
|
||||||
override val users: List<User> get() = _users.values.toList()
|
override val users: List<User> get() = _users.values.toList()
|
||||||
}
|
}
|
||||||
@ -39,3 +40,6 @@ class RPCUserServiceImpl(config: Config) : RPCUserService {
|
|||||||
data class User(val username: String, val password: String, val permissions: Set<String>) {
|
data class User(val username: String, val password: String, val permissions: Set<String>) {
|
||||||
override fun toString(): String = "${javaClass.simpleName}($username, permissions=$permissions)"
|
override fun toString(): String = "${javaClass.simpleName}($username, permissions=$permissions)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun <P : ProtocolLogic<*>> startProtocolPermission(clazz: Class<P>) = "StartProtocol.${clazz.name}"
|
||||||
|
inline fun <reified P : ProtocolLogic<*>> startProtocolPermission(): String = startProtocolPermission(P::class.java)
|
||||||
|
@ -6,6 +6,7 @@ import net.corda.core.node.PluginServiceHub
|
|||||||
import net.corda.core.node.services.TxWritableStorageService
|
import net.corda.core.node.services.TxWritableStorageService
|
||||||
import net.corda.core.protocols.ProtocolLogic
|
import net.corda.core.protocols.ProtocolLogic
|
||||||
import net.corda.core.protocols.ProtocolLogicRefFactory
|
import net.corda.core.protocols.ProtocolLogicRefFactory
|
||||||
|
import net.corda.core.protocols.ProtocolStateMachine
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.node.services.statemachine.ProtocolStateMachineImpl
|
import net.corda.node.services.statemachine.ProtocolStateMachineImpl
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
@ -67,9 +68,9 @@ abstract class ServiceHubInternal : PluginServiceHub {
|
|||||||
* between SMM and the scheduler. That particular problem should also be resolved by the service manager work
|
* between SMM and the scheduler. That particular problem should also be resolved by the service manager work
|
||||||
* itself, at which point this method would not be needed (by the scheduler).
|
* itself, at which point this method would not be needed (by the scheduler).
|
||||||
*/
|
*/
|
||||||
abstract fun <T> startProtocol(logic: ProtocolLogic<T>): ListenableFuture<T>
|
abstract fun <T> startProtocol(logic: ProtocolLogic<T>): ProtocolStateMachine<T>
|
||||||
|
|
||||||
override fun <T : Any> invokeProtocolAsync(logicType: Class<out ProtocolLogic<T>>, vararg args: Any?): ListenableFuture<T> {
|
override fun <T : Any> invokeProtocolAsync(logicType: Class<out ProtocolLogic<T>>, vararg args: Any?): ProtocolStateMachine<T> {
|
||||||
val logicRef = protocolLogicRefFactory.create(logicType, *args)
|
val logicRef = protocolLogicRefFactory.create(logicType, *args)
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
val logic = protocolLogicRefFactory.toProtocolLogic(logicRef) as ProtocolLogic<T>
|
val logic = protocolLogicRefFactory.toProtocolLogic(logicRef) as ProtocolLogic<T>
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package net.corda.node.services.messaging
|
package net.corda.node.services.messaging
|
||||||
|
|
||||||
import net.corda.core.contracts.ClientToServiceCommand
|
|
||||||
import net.corda.core.contracts.ContractState
|
import net.corda.core.contracts.ContractState
|
||||||
import net.corda.core.contracts.StateAndRef
|
import net.corda.core.contracts.StateAndRef
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
@ -8,8 +7,10 @@ import net.corda.core.node.NodeInfo
|
|||||||
import net.corda.core.node.services.NetworkMapCache
|
import net.corda.core.node.services.NetworkMapCache
|
||||||
import net.corda.core.node.services.StateMachineTransactionMapping
|
import net.corda.core.node.services.StateMachineTransactionMapping
|
||||||
import net.corda.core.node.services.Vault
|
import net.corda.core.node.services.Vault
|
||||||
|
import net.corda.core.protocols.ProtocolLogic
|
||||||
import net.corda.core.protocols.StateMachineRunId
|
import net.corda.core.protocols.StateMachineRunId
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
|
import net.corda.core.utilities.ProgressTracker
|
||||||
import net.corda.node.services.statemachine.ProtocolStateMachineImpl
|
import net.corda.node.services.statemachine.ProtocolStateMachineImpl
|
||||||
import net.corda.node.services.statemachine.StateMachineManager
|
import net.corda.node.services.statemachine.StateMachineManager
|
||||||
import net.corda.node.utilities.AddOrRemove
|
import net.corda.node.utilities.AddOrRemove
|
||||||
@ -54,31 +55,9 @@ sealed class StateMachineUpdate(val id: StateMachineRunId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class TransactionBuildResult {
|
|
||||||
/**
|
|
||||||
* State indicating that a protocol is managing this request, and that the client should track protocol state machine
|
|
||||||
* updates for further information. The monitor will separately receive notification of the state machine having been
|
|
||||||
* added, as it would any other state machine. This response is used solely to enable the monitor to identify
|
|
||||||
* the state machine (and its progress) as associated with the request.
|
|
||||||
*
|
|
||||||
* @param transaction the transaction created as a result, in the case where the protocol has completed.
|
|
||||||
*/
|
|
||||||
class ProtocolStarted(val id: StateMachineRunId, val transaction: SignedTransaction?, val message: String?) : TransactionBuildResult() {
|
|
||||||
override fun toString() = "Started($message)"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* State indicating the action undertaken failed, either directly (it is not something which requires a
|
|
||||||
* state machine), or before a state machine was started.
|
|
||||||
*/
|
|
||||||
class Failed(val message: String?) : TransactionBuildResult() {
|
|
||||||
override fun toString() = "Failed($message)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RPC operations that the node exposes to clients using the Java client library. These can be called from
|
* RPC operations that the node exposes to clients using the Java client library. These can be called from
|
||||||
* client apps and are implemented by the node in the [ServerRPCOps] class.
|
* client apps and are implemented by the node in the [CordaRPCOpsImpl] class.
|
||||||
*/
|
*/
|
||||||
interface CordaRPCOps : RPCOps {
|
interface CordaRPCOps : RPCOps {
|
||||||
/**
|
/**
|
||||||
@ -113,15 +92,17 @@ interface CordaRPCOps : RPCOps {
|
|||||||
fun networkMapUpdates(): Pair<List<NodeInfo>, Observable<NetworkMapCache.MapChange>>
|
fun networkMapUpdates(): Pair<List<NodeInfo>, Observable<NetworkMapCache.MapChange>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes the given command if the user is permissioned to do so, possibly triggering cash creation etc.
|
* Start the given protocol with the given arguments, returning an [Observable] with a single observation of the
|
||||||
* 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.
|
* result of running the protocol.
|
||||||
*/
|
*/
|
||||||
fun executeCommand(command: ClientToServiceCommand): TransactionBuildResult
|
@RPCReturnsObservables
|
||||||
|
fun <T: Any> startProtocolDynamic(logicType: Class<out ProtocolLogic<T>>, vararg args: Any?): ProtocolHandle<T>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns Node's identity, assuming this will not change while the node is running.
|
* Returns Node's identity, assuming this will not change while the node is running.
|
||||||
*/
|
*/
|
||||||
fun nodeIdentity(): NodeInfo
|
fun nodeIdentity(): NodeInfo
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Add note(s) to an existing Vault transaction
|
* Add note(s) to an existing Vault transaction
|
||||||
*/
|
*/
|
||||||
@ -132,3 +113,50 @@ interface CordaRPCOps : RPCOps {
|
|||||||
*/
|
*/
|
||||||
fun getVaultTransactionNotes(txnId: SecureHash): Iterable<String>
|
fun getVaultTransactionNotes(txnId: SecureHash): Iterable<String>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These allow type safe invocations of protocols from Kotlin, e.g.:
|
||||||
|
*
|
||||||
|
* val rpc: CordaRPCOps = (..)
|
||||||
|
* rpc.startProtocol(::ResolveTransactionsProtocol, setOf<SecureHash>(), aliceIdentity)
|
||||||
|
*
|
||||||
|
* Note that the passed in constructor function is only used for unification of other type parameters and reification of
|
||||||
|
* the Class instance of the protocol. This could be changed to use the constructor function directly.
|
||||||
|
*/
|
||||||
|
inline fun <T : Any, reified R : ProtocolLogic<T>> CordaRPCOps.startProtocol(
|
||||||
|
@Suppress("UNUSED_PARAMETER")
|
||||||
|
protocolConstructor: () -> R
|
||||||
|
) = startProtocolDynamic(R::class.java)
|
||||||
|
inline fun <T : Any, A, reified R : ProtocolLogic<T>> CordaRPCOps.startProtocol(
|
||||||
|
@Suppress("UNUSED_PARAMETER")
|
||||||
|
protocolConstructor: (A) -> R,
|
||||||
|
arg0: A
|
||||||
|
) = startProtocolDynamic(R::class.java, arg0)
|
||||||
|
inline fun <T : Any, A, B, reified R : ProtocolLogic<T>> CordaRPCOps.startProtocol(
|
||||||
|
@Suppress("UNUSED_PARAMETER")
|
||||||
|
protocolConstructor: (A, B) -> R,
|
||||||
|
arg0: A,
|
||||||
|
arg1: B
|
||||||
|
) = startProtocolDynamic(R::class.java, arg0, arg1)
|
||||||
|
inline fun <T : Any, A, B, C, reified R: ProtocolLogic<T>> CordaRPCOps.startProtocol(
|
||||||
|
@Suppress("UNUSED_PARAMETER")
|
||||||
|
protocolConstructor: (A, B, C) -> R,
|
||||||
|
arg0: A,
|
||||||
|
arg1: B,
|
||||||
|
arg2: C
|
||||||
|
) = startProtocolDynamic(R::class.java, arg0, arg1, arg2)
|
||||||
|
inline fun <T : Any, A, B, C, D, reified R : ProtocolLogic<T>> CordaRPCOps.startProtocol(
|
||||||
|
@Suppress("UNUSED_PARAMETER")
|
||||||
|
protocolConstructor: (A, B, C, D) -> R,
|
||||||
|
arg0: A,
|
||||||
|
arg1: B,
|
||||||
|
arg2: C,
|
||||||
|
arg3: D
|
||||||
|
) = startProtocolDynamic(R::class.java, arg0, arg1, arg2, arg3)
|
||||||
|
|
||||||
|
data class ProtocolHandle<A>(
|
||||||
|
val id: StateMachineRunId,
|
||||||
|
val progress: Observable<ProgressTracker.Change>,
|
||||||
|
val returnValue: Observable<A>
|
||||||
|
)
|
||||||
|
|
||||||
|
@ -28,6 +28,7 @@ import net.corda.core.serialization.*
|
|||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.transactions.WireTransaction
|
import net.corda.core.transactions.WireTransaction
|
||||||
import net.corda.node.services.User
|
import net.corda.node.services.User
|
||||||
|
import net.corda.protocols.CashProtocolResult
|
||||||
import net.i2p.crypto.eddsa.EdDSAPrivateKey
|
import net.i2p.crypto.eddsa.EdDSAPrivateKey
|
||||||
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
||||||
import org.objenesis.strategy.StdInstantiatorStrategy
|
import org.objenesis.strategy.StdInstantiatorStrategy
|
||||||
@ -173,8 +174,8 @@ private class RPCKryo(observableSerializer: Serializer<Observable<Any>>? = null)
|
|||||||
register(Cash.Clauses.ConserveAmount::class.java)
|
register(Cash.Clauses.ConserveAmount::class.java)
|
||||||
register(listOf(Unit).javaClass) // SingletonList
|
register(listOf(Unit).javaClass) // SingletonList
|
||||||
register(setOf(Unit).javaClass) // SingletonSet
|
register(setOf(Unit).javaClass) // SingletonSet
|
||||||
register(TransactionBuildResult.ProtocolStarted::class.java)
|
register(CashProtocolResult.Success::class.java)
|
||||||
register(TransactionBuildResult.Failed::class.java)
|
register(CashProtocolResult.Failed::class.java)
|
||||||
register(ServiceEntry::class.java)
|
register(ServiceEntry::class.java)
|
||||||
register(NodeInfo::class.java)
|
register(NodeInfo::class.java)
|
||||||
register(PhysicalLocation::class.java)
|
register(PhysicalLocation::class.java)
|
||||||
@ -204,6 +205,8 @@ private class RPCKryo(observableSerializer: Serializer<Observable<Any>>? = null)
|
|||||||
register(RPCException::class.java)
|
register(RPCException::class.java)
|
||||||
register(Array<StackTraceElement>::class.java, read = { kryo, input -> emptyArray() }, write = { kryo, output, o -> })
|
register(Array<StackTraceElement>::class.java, read = { kryo, input -> emptyArray() }, write = { kryo, output, o -> })
|
||||||
register(Collections.unmodifiableList(emptyList<String>()).javaClass)
|
register(Collections.unmodifiableList(emptyList<String>()).javaClass)
|
||||||
|
register(PermissionException::class.java)
|
||||||
|
register(ProtocolHandle::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper method, attempt to reduce boiler plate code
|
// Helper method, attempt to reduce boiler plate code
|
||||||
|
@ -34,7 +34,7 @@ class RPCUserServiceImplTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `single permission, which is in lower case`() {
|
fun `single permission, which is in lower case`() {
|
||||||
val service = loadWithContents("rpcUsers : [{ user=user1, password=letmein, permissions=[cash] }]")
|
val service = loadWithContents("rpcUsers : [{ user=user1, password=letmein, permissions=[cash] }]")
|
||||||
assertThat(service.getUser("user1")?.permissions).containsOnly("CASH")
|
assertThat(service.getUser("user1")?.permissions).containsOnly("cash")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -12,9 +12,13 @@ import net.corda.node.services.User
|
|||||||
import net.corda.node.services.messaging.CURRENT_RPC_USER
|
import net.corda.node.services.messaging.CURRENT_RPC_USER
|
||||||
import net.corda.node.services.messaging.PermissionException
|
import net.corda.node.services.messaging.PermissionException
|
||||||
import net.corda.node.services.messaging.StateMachineUpdate
|
import net.corda.node.services.messaging.StateMachineUpdate
|
||||||
|
import net.corda.node.services.messaging.startProtocol
|
||||||
import net.corda.node.services.network.NetworkMapService
|
import net.corda.node.services.network.NetworkMapService
|
||||||
|
import net.corda.node.services.startProtocolPermission
|
||||||
import net.corda.node.services.transactions.SimpleNotaryService
|
import net.corda.node.services.transactions.SimpleNotaryService
|
||||||
import net.corda.node.utilities.databaseTransaction
|
import net.corda.node.utilities.databaseTransaction
|
||||||
|
import net.corda.protocols.CashCommand
|
||||||
|
import net.corda.protocols.CashProtocol
|
||||||
import net.corda.testing.expect
|
import net.corda.testing.expect
|
||||||
import net.corda.testing.expectEvents
|
import net.corda.testing.expectEvents
|
||||||
import net.corda.testing.node.MockNetwork
|
import net.corda.testing.node.MockNetwork
|
||||||
@ -44,7 +48,7 @@ class CordaRPCOpsImplTest {
|
|||||||
aliceNode = network.createNode(networkMapAddress = networkMap.info.address)
|
aliceNode = network.createNode(networkMapAddress = networkMap.info.address)
|
||||||
notaryNode = network.createNode(advertisedServices = ServiceInfo(SimpleNotaryService.type), networkMapAddress = networkMap.info.address)
|
notaryNode = network.createNode(advertisedServices = ServiceInfo(SimpleNotaryService.type), networkMapAddress = networkMap.info.address)
|
||||||
rpc = CordaRPCOpsImpl(aliceNode.services, aliceNode.smm, aliceNode.database)
|
rpc = CordaRPCOpsImpl(aliceNode.services, aliceNode.smm, aliceNode.database)
|
||||||
CURRENT_RPC_USER.set(User("user", "pwd", permissions = setOf(CordaRPCOpsImpl.CASH_PERMISSION)))
|
CURRENT_RPC_USER.set(User("user", "pwd", permissions = setOf(startProtocolPermission<CashProtocol>())))
|
||||||
|
|
||||||
stateMachineUpdates = rpc.stateMachinesAndUpdates().second
|
stateMachineUpdates = rpc.stateMachinesAndUpdates().second
|
||||||
transactions = rpc.verifiedTransactions().second
|
transactions = rpc.verifiedTransactions().second
|
||||||
@ -63,8 +67,8 @@ class CordaRPCOpsImplTest {
|
|||||||
|
|
||||||
// Tell the monitoring service node to issue some cash
|
// Tell the monitoring service node to issue some cash
|
||||||
val recipient = aliceNode.info.legalIdentity
|
val recipient = aliceNode.info.legalIdentity
|
||||||
val outEvent = ClientToServiceCommand.IssueCash(Amount(quantity, GBP), ref, recipient, notaryNode.info.notaryIdentity)
|
val outEvent = CashCommand.IssueCash(Amount(quantity, GBP), ref, recipient, notaryNode.info.notaryIdentity)
|
||||||
rpc.executeCommand(outEvent)
|
rpc.startProtocol(::CashProtocol, outEvent)
|
||||||
network.runNetwork()
|
network.runNetwork()
|
||||||
|
|
||||||
val expectedState = Cash.State(Amount(quantity,
|
val expectedState = Cash.State(Amount(quantity,
|
||||||
@ -101,7 +105,7 @@ class CordaRPCOpsImplTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `issue and move`() {
|
fun `issue and move`() {
|
||||||
|
|
||||||
rpc.executeCommand(ClientToServiceCommand.IssueCash(
|
rpc.startProtocol(::CashProtocol, CashCommand.IssueCash(
|
||||||
amount = Amount(100, USD),
|
amount = Amount(100, USD),
|
||||||
issueRef = OpaqueBytes(ByteArray(1, { 1 })),
|
issueRef = OpaqueBytes(ByteArray(1, { 1 })),
|
||||||
recipient = aliceNode.info.legalIdentity,
|
recipient = aliceNode.info.legalIdentity,
|
||||||
@ -110,7 +114,7 @@ class CordaRPCOpsImplTest {
|
|||||||
|
|
||||||
network.runNetwork()
|
network.runNetwork()
|
||||||
|
|
||||||
rpc.executeCommand(ClientToServiceCommand.PayCash(
|
rpc.startProtocol(::CashProtocol, CashCommand.PayCash(
|
||||||
amount = Amount(100, Issued(PartyAndReference(aliceNode.info.legalIdentity, OpaqueBytes(ByteArray(1, { 1 }))), USD)),
|
amount = Amount(100, Issued(PartyAndReference(aliceNode.info.legalIdentity, OpaqueBytes(ByteArray(1, { 1 }))), USD)),
|
||||||
recipient = aliceNode.info.legalIdentity
|
recipient = aliceNode.info.legalIdentity
|
||||||
))
|
))
|
||||||
@ -182,7 +186,7 @@ class CordaRPCOpsImplTest {
|
|||||||
fun `cash command by user not permissioned for cash`() {
|
fun `cash command by user not permissioned for cash`() {
|
||||||
CURRENT_RPC_USER.set(User("user", "pwd", permissions = emptySet()))
|
CURRENT_RPC_USER.set(User("user", "pwd", permissions = emptySet()))
|
||||||
assertThatExceptionOfType(PermissionException::class.java).isThrownBy {
|
assertThatExceptionOfType(PermissionException::class.java).isThrownBy {
|
||||||
rpc.executeCommand(ClientToServiceCommand.IssueCash(
|
rpc.startProtocol(::CashProtocol, CashCommand.IssueCash(
|
||||||
amount = Amount(100, USD),
|
amount = Amount(100, USD),
|
||||||
issueRef = OpaqueBytes(ByteArray(1, { 1 })),
|
issueRef = OpaqueBytes(ByteArray(1, { 1 })),
|
||||||
recipient = aliceNode.info.legalIdentity,
|
recipient = aliceNode.info.legalIdentity,
|
||||||
|
@ -53,7 +53,7 @@ class AttachmentTests {
|
|||||||
network.runNetwork()
|
network.runNetwork()
|
||||||
val f1 = n1.services.startProtocol(FetchAttachmentsProtocol(setOf(id), n0.info.legalIdentity))
|
val f1 = n1.services.startProtocol(FetchAttachmentsProtocol(setOf(id), n0.info.legalIdentity))
|
||||||
network.runNetwork()
|
network.runNetwork()
|
||||||
assertEquals(0, f1.get().fromDisk.size)
|
assertEquals(0, f1.resultFuture.get().fromDisk.size)
|
||||||
|
|
||||||
// Verify it was inserted into node one's store.
|
// Verify it was inserted into node one's store.
|
||||||
val attachment = n1.storage.attachments.openAttachment(id)!!
|
val attachment = n1.storage.attachments.openAttachment(id)!!
|
||||||
@ -62,7 +62,7 @@ class AttachmentTests {
|
|||||||
// Shut down node zero and ensure node one can still resolve the attachment.
|
// Shut down node zero and ensure node one can still resolve the attachment.
|
||||||
n0.stop()
|
n0.stop()
|
||||||
|
|
||||||
val response: FetchDataProtocol.Result<Attachment> = n1.services.startProtocol(FetchAttachmentsProtocol(setOf(id), n0.info.legalIdentity)).get()
|
val response: FetchDataProtocol.Result<Attachment> = n1.services.startProtocol(FetchAttachmentsProtocol(setOf(id), n0.info.legalIdentity)).resultFuture.get()
|
||||||
assertEquals(attachment, response.fromDisk[0])
|
assertEquals(attachment, response.fromDisk[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,7 +75,7 @@ class AttachmentTests {
|
|||||||
network.runNetwork()
|
network.runNetwork()
|
||||||
val f1 = n1.services.startProtocol(FetchAttachmentsProtocol(setOf(hash), n0.info.legalIdentity))
|
val f1 = n1.services.startProtocol(FetchAttachmentsProtocol(setOf(hash), n0.info.legalIdentity))
|
||||||
network.runNetwork()
|
network.runNetwork()
|
||||||
val e = assertFailsWith<FetchDataProtocol.HashNotFound> { rootCauseExceptions { f1.get() } }
|
val e = assertFailsWith<FetchDataProtocol.HashNotFound> { rootCauseExceptions { f1.resultFuture.get() } }
|
||||||
assertEquals(hash, e.requested)
|
assertEquals(hash, e.requested)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,7 +107,7 @@ class AttachmentTests {
|
|||||||
val f1 = n1.services.startProtocol(FetchAttachmentsProtocol(setOf(id), n0.info.legalIdentity))
|
val f1 = n1.services.startProtocol(FetchAttachmentsProtocol(setOf(id), n0.info.legalIdentity))
|
||||||
network.runNetwork()
|
network.runNetwork()
|
||||||
assertFailsWith<FetchDataProtocol.DownloadedVsRequestedDataMismatch> {
|
assertFailsWith<FetchDataProtocol.DownloadedVsRequestedDataMismatch> {
|
||||||
rootCauseExceptions { f1.get() }
|
rootCauseExceptions { f1.resultFuture.get() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
package net.corda.node.services
|
package net.corda.node.services
|
||||||
|
|
||||||
import com.codahale.metrics.MetricRegistry
|
import com.codahale.metrics.MetricRegistry
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
|
||||||
import net.corda.core.crypto.Party
|
import net.corda.core.crypto.Party
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.core.node.services.*
|
import net.corda.core.node.services.*
|
||||||
import net.corda.core.protocols.ProtocolLogic
|
import net.corda.core.protocols.ProtocolLogic
|
||||||
import net.corda.core.protocols.ProtocolLogicRefFactory
|
import net.corda.core.protocols.ProtocolLogicRefFactory
|
||||||
|
import net.corda.core.protocols.ProtocolStateMachine
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.node.serialization.NodeClock
|
import net.corda.node.serialization.NodeClock
|
||||||
import net.corda.node.services.api.MessagingServiceInternal
|
import net.corda.node.services.api.MessagingServiceInternal
|
||||||
@ -79,7 +79,7 @@ open class MockServiceHubInternal(
|
|||||||
|
|
||||||
override fun recordTransactions(txs: Iterable<SignedTransaction>) = recordTransactionsInternal(txStorageService, txs)
|
override fun recordTransactions(txs: Iterable<SignedTransaction>) = recordTransactionsInternal(txStorageService, txs)
|
||||||
|
|
||||||
override fun <T> startProtocol(logic: ProtocolLogic<T>): ListenableFuture<T> = smm.add(logic).resultFuture
|
override fun <T> startProtocol(logic: ProtocolLogic<T>): ProtocolStateMachine<T> = smm.add(logic)
|
||||||
|
|
||||||
override fun registerProtocolInitiator(markerClass: KClass<*>, protocolFactory: (Party) -> ProtocolLogic<*>) {
|
override fun registerProtocolInitiator(markerClass: KClass<*>, protocolFactory: (Party) -> ProtocolLogic<*>) {
|
||||||
protocolFactories[markerClass.java] = protocolFactory
|
protocolFactories[markerClass.java] = protocolFactory
|
||||||
|
@ -53,7 +53,7 @@ class NotaryChangeTests {
|
|||||||
|
|
||||||
net.runNetwork()
|
net.runNetwork()
|
||||||
|
|
||||||
val newState = future.get()
|
val newState = future.resultFuture.get()
|
||||||
assertEquals(newState.state.notary, newNotary)
|
assertEquals(newState.state.notary, newNotary)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,7 +66,7 @@ class NotaryChangeTests {
|
|||||||
|
|
||||||
net.runNetwork()
|
net.runNetwork()
|
||||||
|
|
||||||
val newState = future.get()
|
val newState = future.resultFuture.get()
|
||||||
assertEquals(newState.state.notary, newNotary)
|
assertEquals(newState.state.notary, newNotary)
|
||||||
val loadedStateA = clientNodeA.services.loadState(newState.ref)
|
val loadedStateA = clientNodeA.services.loadState(newState.ref)
|
||||||
val loadedStateB = clientNodeB.services.loadState(newState.ref)
|
val loadedStateB = clientNodeB.services.loadState(newState.ref)
|
||||||
@ -82,7 +82,7 @@ class NotaryChangeTests {
|
|||||||
|
|
||||||
net.runNetwork()
|
net.runNetwork()
|
||||||
|
|
||||||
val ex = assertFailsWith(ExecutionException::class) { future.get() }
|
val ex = assertFailsWith(ExecutionException::class) { future.resultFuture.get() }
|
||||||
val error = (ex.cause as StateReplacementException).error
|
val error = (ex.cause as StateReplacementException).error
|
||||||
assertTrue(error is StateReplacementRefused)
|
assertTrue(error is StateReplacementRefused)
|
||||||
}
|
}
|
||||||
|
@ -101,7 +101,7 @@ class NotaryServiceTests {
|
|||||||
|
|
||||||
net.runNetwork()
|
net.runNetwork()
|
||||||
|
|
||||||
val ex = assertFailsWith(ExecutionException::class) { future.get() }
|
val ex = assertFailsWith(ExecutionException::class) { future.resultFuture.get() }
|
||||||
val notaryError = (ex.cause as NotaryException).error as NotaryError.Conflict
|
val notaryError = (ex.cause as NotaryException).error as NotaryError.Conflict
|
||||||
assertEquals(notaryError.tx, stx.tx)
|
assertEquals(notaryError.tx, stx.tx)
|
||||||
notaryError.conflict.verified()
|
notaryError.conflict.verified()
|
||||||
@ -110,7 +110,7 @@ class NotaryServiceTests {
|
|||||||
|
|
||||||
private fun runNotaryClient(stx: SignedTransaction): ListenableFuture<DigitalSignature.WithKey> {
|
private fun runNotaryClient(stx: SignedTransaction): ListenableFuture<DigitalSignature.WithKey> {
|
||||||
val protocol = NotaryProtocol.Client(stx)
|
val protocol = NotaryProtocol.Client(stx)
|
||||||
val future = clientNode.services.startProtocol(protocol)
|
val future = clientNode.services.startProtocol(protocol).resultFuture
|
||||||
net.runNetwork()
|
net.runNetwork()
|
||||||
return future
|
return future
|
||||||
}
|
}
|
||||||
|
@ -80,7 +80,7 @@ class ValidatingNotaryServiceTests {
|
|||||||
|
|
||||||
private fun runClient(stx: SignedTransaction): ListenableFuture<DigitalSignature.WithKey> {
|
private fun runClient(stx: SignedTransaction): ListenableFuture<DigitalSignature.WithKey> {
|
||||||
val protocol = NotaryProtocol.Client(stx)
|
val protocol = NotaryProtocol.Client(stx)
|
||||||
val future = clientNode.services.startProtocol(protocol)
|
val future = clientNode.services.startProtocol(protocol).resultFuture
|
||||||
net.runNetwork()
|
net.runNetwork()
|
||||||
return future
|
return future
|
||||||
}
|
}
|
||||||
|
@ -96,7 +96,7 @@ class DataVendingServiceTests {
|
|||||||
|
|
||||||
private class NotifyTxProtocol(val otherParty: Party, val stx: SignedTransaction) : ProtocolLogic<Unit>() {
|
private class NotifyTxProtocol(val otherParty: Party, val stx: SignedTransaction) : ProtocolLogic<Unit>() {
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call() = send(otherParty, NotifyTxRequest(stx, emptySet()))
|
override fun call() = send(otherParty, NotifyTxRequest(stx))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package net.corda.testing.node
|
package net.corda.testing.node
|
||||||
|
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
|
||||||
import net.corda.core.contracts.Attachment
|
import net.corda.core.contracts.Attachment
|
||||||
import net.corda.core.crypto.*
|
import net.corda.core.crypto.*
|
||||||
import net.corda.core.messaging.MessagingService
|
import net.corda.core.messaging.MessagingService
|
||||||
@ -9,6 +8,7 @@ import net.corda.core.node.NodeInfo
|
|||||||
import net.corda.core.node.ServiceHub
|
import net.corda.core.node.ServiceHub
|
||||||
import net.corda.core.node.services.*
|
import net.corda.core.node.services.*
|
||||||
import net.corda.core.protocols.ProtocolLogic
|
import net.corda.core.protocols.ProtocolLogic
|
||||||
|
import net.corda.core.protocols.ProtocolStateMachine
|
||||||
import net.corda.core.protocols.StateMachineRunId
|
import net.corda.core.protocols.StateMachineRunId
|
||||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
@ -38,7 +38,7 @@ import javax.annotation.concurrent.ThreadSafe
|
|||||||
* building chains of transactions and verifying them. It isn't sufficient for testing protocols however.
|
* building chains of transactions and verifying them. It isn't sufficient for testing protocols however.
|
||||||
*/
|
*/
|
||||||
open class MockServices(val key: KeyPair = generateKeyPair()) : ServiceHub {
|
open class MockServices(val key: KeyPair = generateKeyPair()) : ServiceHub {
|
||||||
override fun <T : Any> invokeProtocolAsync(logicType: Class<out ProtocolLogic<T>>, vararg args: Any?): ListenableFuture<T> {
|
override fun <T : Any> invokeProtocolAsync(logicType: Class<out ProtocolLogic<T>>, vararg args: Any?): ProtocolStateMachine<T> {
|
||||||
throw UnsupportedOperationException("not implemented")
|
throw UnsupportedOperationException("not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package net.corda.explorer
|
package net.corda.explorer
|
||||||
|
|
||||||
|
import javafx.stage.Stage
|
||||||
import net.corda.client.mock.EventGenerator
|
import net.corda.client.mock.EventGenerator
|
||||||
import net.corda.client.model.Models
|
import net.corda.client.model.Models
|
||||||
import net.corda.client.model.NodeMonitorModel
|
import net.corda.client.model.NodeMonitorModel
|
||||||
@ -7,12 +8,13 @@ import net.corda.core.node.services.ServiceInfo
|
|||||||
import net.corda.explorer.views.runInFxApplicationThread
|
import net.corda.explorer.views.runInFxApplicationThread
|
||||||
import net.corda.node.driver.PortAllocation
|
import net.corda.node.driver.PortAllocation
|
||||||
import net.corda.node.driver.driver
|
import net.corda.node.driver.driver
|
||||||
import net.corda.node.internal.CordaRPCOpsImpl
|
|
||||||
import net.corda.node.services.User
|
import net.corda.node.services.User
|
||||||
import net.corda.node.services.config.FullNodeConfiguration
|
import net.corda.node.services.config.FullNodeConfiguration
|
||||||
import net.corda.node.services.messaging.ArtemisMessagingComponent
|
import net.corda.node.services.messaging.ArtemisMessagingComponent
|
||||||
|
import net.corda.node.services.messaging.startProtocol
|
||||||
|
import net.corda.node.services.startProtocolPermission
|
||||||
import net.corda.node.services.transactions.SimpleNotaryService
|
import net.corda.node.services.transactions.SimpleNotaryService
|
||||||
import javafx.stage.Stage
|
import net.corda.protocols.CashProtocol
|
||||||
import org.controlsfx.dialog.ExceptionDialog
|
import org.controlsfx.dialog.ExceptionDialog
|
||||||
import tornadofx.App
|
import tornadofx.App
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@ -43,7 +45,7 @@ class Main : App() {
|
|||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
val portAllocation = PortAllocation.Incremental(20000)
|
val portAllocation = PortAllocation.Incremental(20000)
|
||||||
driver(portAllocation = portAllocation) {
|
driver(portAllocation = portAllocation) {
|
||||||
val user = User("user1", "test", permissions = setOf(CordaRPCOpsImpl.CASH_PERMISSION))
|
val user = User("user1", "test", permissions = setOf(startProtocolPermission<CashProtocol>()))
|
||||||
val notary = startNode("Notary", advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
|
val notary = startNode("Notary", advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
|
||||||
val alice = startNode("Alice", rpcUsers = arrayListOf(user))
|
val alice = startNode("Alice", rpcUsers = arrayListOf(user))
|
||||||
val bob = startNode("Bob", rpcUsers = arrayListOf(user))
|
val bob = startNode("Bob", rpcUsers = arrayListOf(user))
|
||||||
@ -67,7 +69,7 @@ fun main(args: Array<String>) {
|
|||||||
notary = notaryNode.nodeInfo.notaryIdentity
|
notary = notaryNode.nodeInfo.notaryIdentity
|
||||||
)
|
)
|
||||||
eventGenerator.clientToServiceCommandGenerator.map { command ->
|
eventGenerator.clientToServiceCommandGenerator.map { command ->
|
||||||
rpcProxy?.executeCommand(command)
|
rpcProxy?.startProtocol(::CashProtocol, command)
|
||||||
}.generate(Random())
|
}.generate(Random())
|
||||||
}
|
}
|
||||||
waitForAllNodesToFinish()
|
waitForAllNodesToFinish()
|
||||||
|
@ -1,5 +1,14 @@
|
|||||||
package net.corda.explorer.views
|
package net.corda.explorer.views
|
||||||
|
|
||||||
|
import javafx.beans.binding.Bindings
|
||||||
|
import javafx.beans.binding.BooleanBinding
|
||||||
|
import javafx.beans.value.ObservableValue
|
||||||
|
import javafx.collections.FXCollections
|
||||||
|
import javafx.collections.ObservableList
|
||||||
|
import javafx.scene.Node
|
||||||
|
import javafx.scene.Parent
|
||||||
|
import javafx.scene.control.*
|
||||||
|
import javafx.util.converter.BigDecimalStringConverter
|
||||||
import net.corda.client.fxutils.map
|
import net.corda.client.fxutils.map
|
||||||
import net.corda.client.model.NetworkIdentityModel
|
import net.corda.client.model.NetworkIdentityModel
|
||||||
import net.corda.client.model.NodeMonitorModel
|
import net.corda.client.model.NodeMonitorModel
|
||||||
@ -10,16 +19,10 @@ import net.corda.core.node.NodeInfo
|
|||||||
import net.corda.core.serialization.OpaqueBytes
|
import net.corda.core.serialization.OpaqueBytes
|
||||||
import net.corda.explorer.model.CashTransaction
|
import net.corda.explorer.model.CashTransaction
|
||||||
import net.corda.node.services.messaging.CordaRPCOps
|
import net.corda.node.services.messaging.CordaRPCOps
|
||||||
import net.corda.node.services.messaging.TransactionBuildResult
|
import net.corda.node.services.messaging.startProtocol
|
||||||
import javafx.beans.binding.Bindings
|
import net.corda.protocols.CashCommand
|
||||||
import javafx.beans.binding.BooleanBinding
|
import net.corda.protocols.CashProtocol
|
||||||
import javafx.beans.value.ObservableValue
|
import net.corda.protocols.CashProtocolResult
|
||||||
import javafx.collections.FXCollections
|
|
||||||
import javafx.collections.ObservableList
|
|
||||||
import javafx.scene.Node
|
|
||||||
import javafx.scene.Parent
|
|
||||||
import javafx.scene.control.*
|
|
||||||
import javafx.util.converter.BigDecimalStringConverter
|
|
||||||
import org.controlsfx.dialog.ExceptionDialog
|
import org.controlsfx.dialog.ExceptionDialog
|
||||||
import tornadofx.View
|
import tornadofx.View
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
@ -126,9 +129,9 @@ class NewTransaction : View() {
|
|||||||
val issueRef = OpaqueBytes(if (issueRefTextField.text.trim().isNotBlank()) issueRefTextField.text.toByteArray() else ByteArray(1, { 1 }))
|
val issueRef = OpaqueBytes(if (issueRefTextField.text.trim().isNotBlank()) issueRefTextField.text.toByteArray() else ByteArray(1, { 1 }))
|
||||||
// TODO : Change these commands into individual RPC methods instead of using executeCommand.
|
// TODO : Change these commands into individual RPC methods instead of using executeCommand.
|
||||||
val command = when (it) {
|
val command = when (it) {
|
||||||
CashTransaction.Issue -> ClientToServiceCommand.IssueCash(Amount(textFormatter.value, currency.value), issueRef, partyBChoiceBox.value.legalIdentity, notary.notaryIdentity)
|
CashTransaction.Issue -> CashCommand.IssueCash(Amount(textFormatter.value, currency.value), issueRef, partyBChoiceBox.value.legalIdentity, notary.notaryIdentity)
|
||||||
CashTransaction.Pay -> ClientToServiceCommand.PayCash(Amount(textFormatter.value, Issued(PartyAndReference(myIdentity.legalIdentity, issueRef), currency.value)), partyBChoiceBox.value.legalIdentity)
|
CashTransaction.Pay -> CashCommand.PayCash(Amount(textFormatter.value, Issued(PartyAndReference(myIdentity.legalIdentity, issueRef), currency.value)), partyBChoiceBox.value.legalIdentity)
|
||||||
CashTransaction.Exit -> ClientToServiceCommand.ExitCash(Amount(textFormatter.value, currency.value), issueRef)
|
CashTransaction.Exit -> CashCommand.ExitCash(Amount(textFormatter.value, currency.value), issueRef)
|
||||||
}
|
}
|
||||||
val dialog = Alert(Alert.AlertType.INFORMATION).apply {
|
val dialog = Alert(Alert.AlertType.INFORMATION).apply {
|
||||||
headerText = null
|
headerText = null
|
||||||
@ -138,15 +141,15 @@ class NewTransaction : View() {
|
|||||||
}
|
}
|
||||||
dialog.show()
|
dialog.show()
|
||||||
runAsync {
|
runAsync {
|
||||||
rpcProxy.executeCommand(command)
|
rpcProxy.startProtocol(::CashProtocol, command).returnValue.toBlocking().first()
|
||||||
}.ui {
|
}.ui {
|
||||||
dialog.contentText = when (it) {
|
dialog.contentText = when (it) {
|
||||||
is TransactionBuildResult.ProtocolStarted -> {
|
is CashProtocolResult.Success -> {
|
||||||
dialog.alertType = Alert.AlertType.INFORMATION
|
dialog.alertType = Alert.AlertType.INFORMATION
|
||||||
dialog.setOnCloseRequest { resetScreen() }
|
dialog.setOnCloseRequest { resetScreen() }
|
||||||
"Transaction Started \nTransaction ID : ${it.transaction?.id} \nMessage : ${it.message}"
|
"Transaction Started \nTransaction ID : ${it.transaction?.id} \nMessage : ${it.message}"
|
||||||
}
|
}
|
||||||
is TransactionBuildResult.Failed -> {
|
is CashProtocolResult.Failed -> {
|
||||||
dialog.alertType = Alert.AlertType.ERROR
|
dialog.alertType = Alert.AlertType.ERROR
|
||||||
it.toString()
|
it.toString()
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user