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

Make AccountInfo.identifier.externalId more usable #105

Open
wants to merge 3 commits 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
Expand Up @@ -26,6 +26,8 @@ object AccountsContractsSchemaV1 : MappedSchema(
data class PersistentAccountInfo(
@Column(name = "identifier", unique = true, nullable = false)
val id: UUID,
@Column(name = "external_id", unique = false, nullable = true)
val externalId: String?,
@Column(name = "name", unique = false, nullable = false)
val name: String,
@Column(name = "host", unique = false, nullable = false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ data class AccountInfo(
return PersistentAccountInfo(
name = name,
host = host,
id = identifier.id
id = identifier.id,
externalId = identifier.externalId
)
} else {
throw IllegalStateException("Cannot construct instance of ${this.javaClass} from Schema: $schema")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ fun accountUUIDCriteria(id: UUID): QueryCriteria {
}
}

/** To query [AccountInfo]s by external ID. */
fun accountExternalIdCriteria(externalId: String): QueryCriteria {
return builder {
val externalIdSelector = PersistentAccountInfo::externalId.equal(externalId)
QueryCriteria.VaultCustomQueryCriteria(externalIdSelector)
}
}

/** To query [ContractState]s by which an account has been allowed to see an an observer. */
fun allowedToSeeCriteria(accountIds: List<UUID>): QueryCriteria {
return builder {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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.accountService
import net.corda.core.contracts.StateAndRef
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StartableByRPC
import java.util.*

/**
* Returns the [AccountInfo]s for a specified external ID.
*
* @property externalId the account external ID to return the [AccountInfo] for
*/
@StartableByRPC
class AccountInfoByExternalId(private val externalId: String) : FlowLogic<List<StateAndRef<AccountInfo>>>() {
@Suspendable
override fun call(): List<StateAndRef<AccountInfo>> {
return accountService.accountInfoByExternalId(externalId)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,21 @@ import java.util.*
*
* @property name the proposed name for the new account.
* @property identifier the proposed identifier for the new account.
* @property externalId the proposed external ID for the new account.
*/
@StartableByService
@StartableByRPC
class CreateAccount private constructor(
private val name: String,
private val identifier: UUID
private val identifier: UUID,
private val externalId: String?
) : FlowLogic<StateAndRef<AccountInfo>>() {

/** Create a new account with a specified [name] but generate a new random [id]. */
constructor(name: String) : this(name, UUID.randomUUID())
constructor(name: String) : this(name, UUID.randomUUID(), null)

/** Create a new account with a specified [name] and [externalId] but generate a new random [id]. */
constructor(name: String, externalId: String? = null) : this(name, UUID.randomUUID(), externalId)

@Suspendable
override fun call(): StateAndRef<AccountInfo> {
Expand All @@ -44,7 +49,7 @@ class CreateAccount private constructor(
val newAccountInfo = AccountInfo(
name = name,
host = ourIdentity,
identifier = UniqueIdentifier(id = identifier)
identifier = UniqueIdentifier(id = identifier, externalId = externalId)
)
val transactionBuilder = TransactionBuilder(notary = notary).apply {
addOutputState(newAccountInfo)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
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.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

/**
* Alternative to [RequestAccountInfoFlow] that matches a hosted Account based on the `linearId.externalId` value.
* Maintaining unique `externalId` values per host node is an application-level concern. An error will be thrown
* if multiple matches are found.
*
* @property `externalId` the `linearId.externalId` value to request [AccountInfo]s for.
* @property session session to request the [AccountInfo] from.
*/
class RequestHostedAccountInfoByExternalIdFlow(val externalId: String, val session: FlowSession) : FlowLogic<AccountInfo?>() {
@Suspendable
override fun call(): AccountInfo? {
val hasAccount = session.sendAndReceive<Boolean>(externalId).unwrap { it }
return if (hasAccount) subFlow(ShareAccountInfoHandlerFlow(session)) else null
}
}

/**
* Responder flow for [RequestHostedAccountInfoByExternalIdFlow].
*/
class RequestHostedAccountInfoByExternalIdHandlerFlow(val otherSession: FlowSession) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
val requestedAccount = otherSession.receive<String>().unwrap {
accountService.accountInfoByExternalId(it).singleOrNull { it.state.data.host == ourIdentity }
}
if (requestedAccount == null) {
otherSession.send(false)
} else {
otherSession.send(true)
subFlow(ShareAccountInfoFlow(requestedAccount, 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 externalId identifier to request the [AccountInfo] for.
* @property host [Party] to request the [AccountInfo] from.
*/
@StartableByRPC
@StartableByService
@InitiatingFlow
class RequestHostedAccountInfoByExternalId(val externalId: String, val host: Party) : FlowLogic<AccountInfo?>() {
@Suspendable
override fun call(): AccountInfo? {
val session = initiateFlow(host)
return subFlow(RequestHostedAccountInfoByExternalIdFlow(externalId, session))
}
}

/**
* Responder flow for [RequestHostedAccountInfoByExternalId].
*/
@InitiatedBy(RequestHostedAccountInfoByExternalId::class)
class RequestHostedAccountInfoByExternalIdHandler(val otherSession: FlowSession) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
subFlow(RequestHostedAccountInfoByExternalIdHandlerFlow(otherSession))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,17 @@ interface AccountService : SerializeAsToken {
*/
fun accountInfo(name: String): List<StateAndRef<AccountInfo>>

/**
* Returns the [AccountInfo]s for an accounts specified by [externalId]. The assumption here is that Account external
* IDs are not guaranteed to be unique at either network or node level as this is the responsibility of the client
* application.
*
* This method may return more than one [AccountInfo].
*
* @param externalId the account external ID to return [AccountInfo]s for.
*/
fun accountInfoByExternalId(externalId: String): List<StateAndRef<AccountInfo>>

/**
* Shares an [AccountInfo] [StateAndRef] with the specified [Party]. The [AccountInfo]is always stored by the
* recipient using [StatesToRecord.ALL_VISIBLE].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,23 @@ class KeyManagementBackedAccountService(val services: AppServiceHub) : AccountSe
}
}

override fun accountInfoByExternalId(externalId: String): List<StateAndRef<AccountInfo>> {
val externalIdCriteria = accountExternalIdCriteria(externalId)
val results = services.vaultService.queryBy<AccountInfo>(
accountBaseCriteria.and(externalIdCriteria)).states
return when (results.size) {
0 -> emptyList()
1 -> listOf(results.single())
else -> {
logger.warn("WARNING: Querying for account by externalId returned more than one account, this is likely " +
"because another node shared an account with this node that has the same name as an " +
"account already created on this node. Filtering the results by host will allow you to access" +
"the AccountInfo you need.")
results
}
}
}

@Suspendable
override fun createAccount(name: String): CordaFuture<StateAndRef<AccountInfo>> {
return flowAwareStartFlow(CreateAccount(name))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,10 @@

</changeSet>

<changeSet author="R3.Corda" id="accounts-contracts-schema-migrations-v1.0-external-id" dbms="h2">
<addColumn tableName="accounts">
<column name="external_id" type="NVARCHAR(255)" />
</addColumn>
</changeSet>

</databaseChangeLog>
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,10 @@

</changeSet>

<changeSet author="R3.Corda" id="accounts-contracts-schema-migrations-v1.0-external-id" dbms="azure,mssql">
<addColumn tableName="accounts">
<column name="external_id" type="NVARCHAR(255)" />
</addColumn>
</changeSet>

</databaseChangeLog>
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,10 @@

</changeSet>

<changeSet author="R3.Corda" id="accounts-contracts-schema-migrations-v1.0-external-id" dbms="oracle">
<addColumn tableName="accounts">
<column name="external_id" type="NVARCHAR(255)" />
</addColumn>
</changeSet>

</databaseChangeLog>
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,10 @@

</changeSet>

<changeSet author="R3.Corda" id="accounts-contracts-schema-migrations-v1.0-external-id" dbms="postgresql">
<addColumn tableName="accounts">
<column name="external_id" type="NVARCHAR(255)" />
</addColumn>
</changeSet>

</databaseChangeLog>
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,30 @@ class AccountInfoTests {
Assert.assertEquals(accountOnNodeB, accountInfoBfromB)
}

@Test
fun `Create and query accounts by externalId`() {
// Create the account with externalId on A
val name = "Account_WithExternalId_On_A"
val externalId = UUID.randomUUID().toString()
val accountInfo = nodeA.startFlow(CreateAccount(name, externalId)).runAndGet(network)

// Check for expected external ID
Assert.assertEquals(externalId, accountInfo.id.externalId)

// Node A will share the created account with Node B
nodeA.startFlow(ShareAccountInfo(accountInfo, listOf(nodeB.identity()))).runAndGet(network)
// Node C requests the created account from A
nodeC.startFlow(RequestAccountInfo(accountInfo.uuid, nodeA.info.legalIdentities.first())).runAndGet(network)

// Check query stack by external ID works for all A, B and C
val accountInfosA = nodeA.startFlow(AccountInfoByExternalId(externalId)).runAndGet(network)
val accountInfosB = nodeB.startFlow(AccountInfoByExternalId(externalId)).runAndGet(network)
val accountInfosC = nodeC.startFlow(AccountInfoByExternalId(externalId)).runAndGet(network)
Assert.assertEquals(accountInfo, accountInfosA.single())
Assert.assertEquals(accountInfo, accountInfosB.single())
Assert.assertEquals(accountInfo, accountInfosC.single())
}

@Test
fun `Accounts not shared are not found`() {
//Validate that the account on C exists first
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,16 +83,20 @@ class RequestAccountInfoTest {
fun `should throw error if the UUID of account passed is wrong and compare the expected account's and actual account's identifier`() {

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

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

//To avail the account info of account B for node A, passing UUID of account C which is wrong UUID
val accountBInfo = nodeA.startFlow(RequestAccountInfo(accountC.uuid, nodeB.info.legalIdentities.first())).runAndGet(network)
val accountBInfoByExt = nodeA.startFlow(RequestHostedAccountInfoByExternalId(
accountC.state.data.identifier.externalId!!,
nodeB.info.legalIdentities.first())).runAndGet(network)

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

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