Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a new flow to request for accountInfo by name from a particular host #94

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.r3.corda.lib.accounts.workflows.flows

import co.paralleluniverse.fibers.Suspendable
import com.r3.corda.lib.accounts.contracts.states.AccountInfo
import com.r3.corda.lib.accounts.workflows.internal.accountService
import net.corda.core.contracts.StateAndRef
import net.corda.core.flows.*
import net.corda.core.identity.Party
import net.corda.core.node.StatesToRecord
import net.corda.core.utilities.unwrap

/**
* This flow can be used to check whether an account is hosted by a particular host by passing account name. If it is then the host will share
* the account info with the requester. This flow should be used in a situation where UUID is difficult to obtain for a given
* account. For e.g: a node can create accounts for its employees using SSN/NIN or employeeId
*
* @property accountName account name to request the [AccountInfo] for hosted at [host] node.
* @property host session to request the [AccountInfo] from.
*/
class RequestAccountInfoByNameFlow(private val accountName: String, val host: FlowSession) : FlowLogic<AccountInfo?>() {
@Suspendable
override fun call(): AccountInfo? {
Copy link

@adelrustum adelrustum Apr 16, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If host.counterParty == ourIdentity, then just fetch the account from the current node.

val hasAccount = host.sendAndReceive<Boolean>(accountName).unwrap { it }
return if (hasAccount) subFlow(ShareAccountInfoHandlerFlow(host)) else null
}
}

/**
* Responder flow for [RequestAccountInfoByNameFlow].
*/
class RequestAccountInfoByNameHandlerFlow(val otherSession: FlowSession) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
val accountName = otherSession.receive(String::class.java).unwrap {it }
val response = serviceHub.accountService.accountInfo(accountName).find { it.state.data.host == ourIdentity }
if (response == null) {
otherSession.send(false)
} else {
otherSession.send(true)
subFlow(ShareAccountInfoFlow(response, listOf(otherSession)))
}
}
}

// Initiating versions of the above flows.

/**
* Shares an [AccountInfo] [StateAndRef] with the supplied [Party]s. The [AccountInfo] is always stored using
* [StatesToRecord.ALL_VISIBLE].
*
* @property accountName account name to request the [AccountInfo] for.
* @property host [Party] to request the [AccountInfo] from.
*/
@StartableByRPC
@StartableByService
@InitiatingFlow
class RequestAccountInfoByName(private val accountName: String, val host: Party) : FlowLogic<AccountInfo?>() {
@Suspendable
override fun call(): AccountInfo? {
Copy link

@adelrustum adelrustum Apr 16, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If ourIdentity == host, then just fetch the account from the current node.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the comments.
This flow is specifically to request accountInfo from another node and not from the caller's local node itself. The assumption here is that the caller is aware that an account with name "charlie" exists on host node "Alice". (obviously the caller will try to search for account with name "Charlie" on its local vault first)

Copy link

@adelrustum adelrustum Apr 16, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my humble opinion, you cannot assume how your flow will be used; your code should take care of all cases.

For instance, in the RequestKeyForAccountFlow (which comes with the library):

  1. The flow returns the key immediately if the host is the initiator (see here)
  2. Otherwise, it contacts the host node (see here)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, the design of this flow is kept in consistent with the design of RequestAccountInfoFlow (here)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see; now I'm curious why they did it that way 🙂

val session = initiateFlow(host)
return subFlow(RequestAccountInfoByNameFlow(accountName, session))
}
}

/**
* Responder flow for [RequestAccountInfoByName].
*/
@InitiatedBy(RequestAccountInfoByName::class)
class RequestAccountInfoByNameHandler(val otherSession: FlowSession) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
subFlow(RequestAccountInfoByNameHandlerFlow(otherSession))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package com.r3.corda.lib.accounts.workflows.test

import com.r3.corda.lib.accounts.workflows.flows.*
import net.corda.testing.common.internal.testNetworkParameters
import net.corda.testing.node.MockNetwork
import net.corda.testing.node.MockNetworkParameters
import net.corda.testing.node.StartedMockNode
import net.corda.testing.node.TestCordapp
import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import java.lang.AssertionError
import kotlin.test.assertFailsWith

class RequestAccountInfoByNameTest {

lateinit var network: MockNetwork
lateinit var nodeA: StartedMockNode
lateinit var nodeB: StartedMockNode
lateinit var nodeC: StartedMockNode

@Before
fun setup() {
network = MockNetwork(
MockNetworkParameters(
networkParameters = testNetworkParameters(minimumPlatformVersion = 4),
cordappsForAllNodes = listOf(
TestCordapp.findCordapp("com.r3.corda.lib.accounts.contracts"),
TestCordapp.findCordapp("com.r3.corda.lib.accounts.workflows")
)
)
)
nodeA = network.createPartyNode()
nodeB = network.createPartyNode()
nodeC = network.createPartyNode()

network.runNetwork()
}

@After
fun tearDown() {
network.stopNodes()
}

fun StartedMockNode.identity() = info.legalIdentities.first()

/*
Should return the account info to the host which been requested
*/

@Test
fun `should send account info to requester`() {

//Create an account in host B
val accountB = nodeB.startFlow(CreateAccount("Test_AccountB")).runAndGet(network)

//Call RequestAccountInfoByName from host A so that host A can request accountB's info
val accountBInfo = nodeA.startFlow(RequestAccountInfoByName("Test_AccountB", nodeB.info.legalIdentities.first())).runAndGet(network)
print(accountBInfo)

//Checking if accountBInfo's name will be equal to the name with which the account is created
Assert.assertEquals(accountBInfo?.name, "Test_AccountB")

//Checking if accountBInfo's name will be equal to the name of the account created
Assert.assertEquals(accountBInfo?.name, accountB.state.data.name)
}

/*
Should throw error if the requested account is not of the host and compare expected account's name with
actual account's name
*/


@Test(expected = AssertionError::class)
fun `should throw error if the name of account passed is wrong and compare the expected account's and actual account's name`() {

//Create an account in host B
val accountB = nodeB.startFlow(CreateAccount("Test_AccountB")).runAndGet(network)

//Create an account in host C
val accountC = nodeC.startFlow(CreateAccount("Test_AccountC")).runAndGet(network)

//To avail the account info of account B for node A, passing name of account C which is wrong name
val accountBInfo = nodeA.startFlow(RequestAccountInfoByName(accountC.state.data.name, nodeB.info.legalIdentities.first())).runAndGet(network)

//Comparing actual account's name with expected account(account B)'s name
val resultOfAccountIdentifierComparison = Assert.assertEquals(accountBInfo?.name, accountB.state.data.name)

//result will throw error since the name comparison do not match
assertFailsWith<AssertionError> { resultOfAccountIdentifierComparison }

}

/*
Should throw error if the host passed is wrong and compare expected account's name with
actual account's name
*/


@Test(expected = AssertionError::class)
fun `should throw error if the account's host is wrong and compare expected account's and actual account's name`() {

//Create an account in host A
val accountA = nodeA.startFlow(CreateAccount("Test_AccountA")).runAndGet(network)

//To get the account info of accountA, passing host as C which is the wrong host
val accountAInfo = nodeB.startFlow(RequestAccountInfoByName(accountA.state.data.name, nodeC.info.legalIdentities.first())).runAndGet(network)

//Comparing actual account's name with expected account(account A)'s name
val resultOfAccountIdentifierComparison = Assert.assertEquals(accountAInfo?.name, accountA.state.data.name)

//result will throw error since the name comparison do not match
assertFailsWith<AssertionError> { resultOfAccountIdentifierComparison }
}

/*
This testcase check when pass wrong name of the account, the result will be null
*/

@Test
fun `should return null if account is not found when searching by name`() {

//Create an account in host C
val accountC = nodeC.startFlow(CreateAccount("Test_AccountC")).runAndGet(network)

//To avail the account info of account B for node A, passing name of account C which will throw an error
val accountBInfo = nodeA.startFlow(RequestAccountInfoByName(accountC.state.data.name, nodeB.info.legalIdentities.first())).runAndGet(network)
print(accountBInfo)

//accountBInfo will be null if the name of account entered is wrong
Assert.assertEquals(accountBInfo,null)

}

}