mirror of
https://github.com/corda/corda.git
synced 2024-12-24 15:16:45 +00:00
Assorted set of clean ups (#4039)
This commit is contained in:
parent
0621efe7c6
commit
39434dcbec
@ -24,14 +24,14 @@ import org.junit.After
|
|||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
|
||||||
// A dummy reference state contract.
|
// A dummy reference state contract.
|
||||||
internal class RefState : Contract {
|
internal class RefState : Contract {
|
||||||
companion object {
|
companion object {
|
||||||
val CONTRACT_ID = "net.corda.core.flows.RefState"
|
const val CONTRACT_ID = "net.corda.core.flows.RefState"
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun verify(tx: LedgerTransaction) = Unit
|
override fun verify(tx: LedgerTransaction) = Unit
|
||||||
|
|
||||||
data class State(val owner: Party, val version: Int = 0, override val linearId: UniqueIdentifier = UniqueIdentifier()) : LinearState {
|
data class State(val owner: Party, val version: Int = 0, override val linearId: UniqueIdentifier = UniqueIdentifier()) : LinearState {
|
||||||
override val participants: List<AbstractParty> get() = listOf(owner)
|
override val participants: List<AbstractParty> get() = listOf(owner)
|
||||||
fun update() = copy(version = version + 1)
|
fun update() = copy(version = version + 1)
|
||||||
@ -46,34 +46,32 @@ internal class CreateRefState : FlowLogic<SignedTransaction>() {
|
|||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call(): SignedTransaction {
|
override fun call(): SignedTransaction {
|
||||||
val notary = serviceHub.networkMapCache.notaryIdentities.first()
|
val notary = serviceHub.networkMapCache.notaryIdentities.first()
|
||||||
return subFlow(FinalityFlow(
|
val stx = serviceHub.signInitialTransaction(TransactionBuilder(notary = notary).apply {
|
||||||
transaction = serviceHub.signInitialTransaction(TransactionBuilder(notary = notary).apply {
|
|
||||||
addOutputState(RefState.State(ourIdentity), RefState.CONTRACT_ID)
|
addOutputState(RefState.State(ourIdentity), RefState.CONTRACT_ID)
|
||||||
addCommand(RefState.Create(), listOf(ourIdentity.owningKey))
|
addCommand(RefState.Create(), listOf(ourIdentity.owningKey))
|
||||||
})
|
})
|
||||||
))
|
return subFlow(FinalityFlow(stx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A flow to update a specific reference state.
|
// A flow to update a specific reference state.
|
||||||
internal class UpdateRefState(val stateAndRef: StateAndRef<ContractState>) : FlowLogic<SignedTransaction>() {
|
internal class UpdateRefState(private val stateAndRef: StateAndRef<ContractState>) : FlowLogic<SignedTransaction>() {
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call(): SignedTransaction {
|
override fun call(): SignedTransaction {
|
||||||
val notary = serviceHub.networkMapCache.notaryIdentities.first()
|
val notary = serviceHub.networkMapCache.notaryIdentities.first()
|
||||||
return subFlow(FinalityFlow(
|
val stx = serviceHub.signInitialTransaction(TransactionBuilder(notary = notary).apply {
|
||||||
transaction = serviceHub.signInitialTransaction(TransactionBuilder(notary = notary).apply {
|
|
||||||
addInputState(stateAndRef)
|
addInputState(stateAndRef)
|
||||||
addOutputState((stateAndRef.state.data as RefState.State).update(), RefState.CONTRACT_ID)
|
addOutputState((stateAndRef.state.data as RefState.State).update(), RefState.CONTRACT_ID)
|
||||||
addCommand(RefState.Update(), listOf(ourIdentity.owningKey))
|
addCommand(RefState.Update(), listOf(ourIdentity.owningKey))
|
||||||
})
|
})
|
||||||
))
|
return subFlow(FinalityFlow(stx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A set of flows to share a stateref with all other nodes in the mock network.
|
// A set of flows to share a stateref with all other nodes in the mock network.
|
||||||
internal object ShareRefState {
|
internal object ShareRefState {
|
||||||
@InitiatingFlow
|
@InitiatingFlow
|
||||||
class Initiator(val stateAndRef: StateAndRef<ContractState>) : FlowLogic<Unit>() {
|
class Initiator(private val stateAndRef: StateAndRef<ContractState>) : FlowLogic<Unit>() {
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call() {
|
override fun call() {
|
||||||
val sessions = serviceHub.networkMapCache.allNodes.flatMap { it.legalIdentities }.map { initiateFlow(it) }
|
val sessions = serviceHub.networkMapCache.allNodes.flatMap { it.legalIdentities }.map { initiateFlow(it) }
|
||||||
@ -85,7 +83,7 @@ internal object ShareRefState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@InitiatedBy(ShareRefState.Initiator::class)
|
@InitiatedBy(ShareRefState.Initiator::class)
|
||||||
class Responder(val otherSession: FlowSession) : FlowLogic<Unit>() {
|
class Responder(private val otherSession: FlowSession) : FlowLogic<Unit>() {
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call() {
|
override fun call() {
|
||||||
logger.info("Receiving dependencies.")
|
logger.info("Receiving dependencies.")
|
||||||
@ -99,7 +97,7 @@ internal object ShareRefState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A flow to use a reference state in another transaction.
|
// A flow to use a reference state in another transaction.
|
||||||
internal class UseRefState(val linearId: UniqueIdentifier) : FlowLogic<SignedTransaction>() {
|
internal class UseRefState(private val linearId: UniqueIdentifier) : FlowLogic<SignedTransaction>() {
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call(): SignedTransaction {
|
override fun call(): SignedTransaction {
|
||||||
val notary = serviceHub.networkMapCache.notaryIdentities.first()
|
val notary = serviceHub.networkMapCache.notaryIdentities.first()
|
||||||
@ -108,14 +106,12 @@ internal class UseRefState(val linearId: UniqueIdentifier) : FlowLogic<SignedTra
|
|||||||
relevancyStatus = Vault.RelevancyStatus.ALL
|
relevancyStatus = Vault.RelevancyStatus.ALL
|
||||||
)
|
)
|
||||||
val referenceState = serviceHub.vaultService.queryBy<ContractState>(query).states.single()
|
val referenceState = serviceHub.vaultService.queryBy<ContractState>(query).states.single()
|
||||||
return subFlow(FinalityFlow(
|
val stx = serviceHub.signInitialTransaction(TransactionBuilder(notary = notary).apply {
|
||||||
transaction = serviceHub.signInitialTransaction(TransactionBuilder(notary = notary).apply {
|
|
||||||
@Suppress("DEPRECATION") // To be removed when feature is finalised.
|
|
||||||
addReferenceState(referenceState.referenced())
|
addReferenceState(referenceState.referenced())
|
||||||
addOutputState(DummyState(), DummyContract.PROGRAM_ID)
|
addOutputState(DummyState(), DummyContract.PROGRAM_ID)
|
||||||
addCommand(DummyContract.Commands.Create(), listOf(ourIdentity.owningKey))
|
addCommand(DummyContract.Commands.Create(), listOf(ourIdentity.owningKey))
|
||||||
})
|
})
|
||||||
))
|
return subFlow(FinalityFlow(stx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,37 +1,42 @@
|
|||||||
package net.corda.core.flows.mixins
|
package net.corda.core.flows.mixins
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
|
import com.natpryce.hamkrest.MatchResult
|
||||||
import com.natpryce.hamkrest.Matcher
|
import com.natpryce.hamkrest.Matcher
|
||||||
import com.natpryce.hamkrest.equalTo
|
import com.natpryce.hamkrest.equalTo
|
||||||
import net.corda.core.flows.FinalityFlow
|
import net.corda.core.flows.FinalityFlow
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
import net.corda.core.flows.StartableByRPC
|
import net.corda.core.flows.StartableByRPC
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.internal.FlowStateMachine
|
||||||
import net.corda.core.messaging.CordaRPCOps
|
import net.corda.core.messaging.CordaRPCOps
|
||||||
|
import net.corda.core.messaging.FlowHandle
|
||||||
import net.corda.core.messaging.startFlow
|
import net.corda.core.messaging.startFlow
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.testing.core.singleIdentity
|
import net.corda.testing.core.singleIdentity
|
||||||
import net.corda.testing.node.internal.TestStartedNode
|
import net.corda.testing.node.internal.TestStartedNode
|
||||||
|
|
||||||
interface WithFinality : WithMockNet {
|
interface WithFinality : WithMockNet {
|
||||||
|
|
||||||
//region Operations
|
//region Operations
|
||||||
fun TestStartedNode.finalise(stx: SignedTransaction, vararg additionalParties: Party) =
|
fun TestStartedNode.finalise(stx: SignedTransaction, vararg additionalParties: Party): FlowStateMachine<SignedTransaction> {
|
||||||
startFlowAndRunNetwork(FinalityFlow(stx, additionalParties.toSet()))
|
return startFlowAndRunNetwork(FinalityFlow(stx, additionalParties.toSet()))
|
||||||
|
}
|
||||||
|
|
||||||
fun TestStartedNode.getValidatedTransaction(stx: SignedTransaction) =
|
fun TestStartedNode.getValidatedTransaction(stx: SignedTransaction): SignedTransaction {
|
||||||
services.validatedTransactions.getTransaction(stx.id)!!
|
return services.validatedTransactions.getTransaction(stx.id)!!
|
||||||
|
}
|
||||||
|
|
||||||
fun CordaRPCOps.finalise(stx: SignedTransaction, vararg parties: Party) =
|
fun CordaRPCOps.finalise(stx: SignedTransaction, vararg parties: Party): FlowHandle<SignedTransaction> {
|
||||||
startFlow(::FinalityInvoker, stx, parties.toSet())
|
return startFlow(::FinalityInvoker, stx, parties.toSet()).andRunNetwork()
|
||||||
.andRunNetwork()
|
}
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
//region Matchers
|
//region Matchers
|
||||||
fun visibleTo(other: TestStartedNode) = object : Matcher<SignedTransaction> {
|
fun visibleTo(other: TestStartedNode) = object : Matcher<SignedTransaction> {
|
||||||
override val description = "has a transaction visible to ${other.info.singleIdentity()}"
|
override val description = "has a transaction visible to ${other.info.singleIdentity()}"
|
||||||
override fun invoke(actual: SignedTransaction) =
|
override fun invoke(actual: SignedTransaction): MatchResult {
|
||||||
equalTo(actual)(other.getValidatedTransaction(actual))
|
return equalTo(actual)(other.getValidatedTransaction(actual))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
|
@ -66,11 +66,11 @@ public class IOUFlow extends FlowLogic<Void> {
|
|||||||
final SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder);
|
final SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder);
|
||||||
|
|
||||||
// Creating a session with the other party.
|
// Creating a session with the other party.
|
||||||
FlowSession otherpartySession = initiateFlow(otherParty);
|
FlowSession otherPartySession = initiateFlow(otherParty);
|
||||||
|
|
||||||
// Obtaining the counterparty's signature.
|
// Obtaining the counterparty's signature.
|
||||||
SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow(
|
SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow(
|
||||||
signedTx, ImmutableList.of(otherpartySession), CollectSignaturesFlow.tracker()));
|
signedTx, ImmutableList.of(otherPartySession), CollectSignaturesFlow.tracker()));
|
||||||
|
|
||||||
// Finalising the transaction.
|
// Finalising the transaction.
|
||||||
subFlow(new FinalityFlow(fullySignedTx));
|
subFlow(new FinalityFlow(fullySignedTx));
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
@file:Suppress("MemberVisibilityCanBePrivate")
|
||||||
|
|
||||||
package net.corda.docs.kotlin.tutorial.helloworld
|
package net.corda.docs.kotlin.tutorial.helloworld
|
||||||
|
|
||||||
import net.corda.core.contracts.ContractState
|
import net.corda.core.contracts.ContractState
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
@file:Suppress("MemberVisibilityCanBePrivate")
|
||||||
|
|
||||||
package net.corda.docs.kotlin.tutorial.twoparty
|
package net.corda.docs.kotlin.tutorial.twoparty
|
||||||
|
|
||||||
// DOCSTART 01
|
// DOCSTART 01
|
||||||
@ -48,10 +50,10 @@ class IOUFlow(val iouValue: Int,
|
|||||||
val signedTx = serviceHub.signInitialTransaction(txBuilder)
|
val signedTx = serviceHub.signInitialTransaction(txBuilder)
|
||||||
|
|
||||||
// Creating a session with the other party.
|
// Creating a session with the other party.
|
||||||
val otherpartySession = initiateFlow(otherParty)
|
val otherPartySession = initiateFlow(otherParty)
|
||||||
|
|
||||||
// Obtaining the counterparty's signature.
|
// Obtaining the counterparty's signature.
|
||||||
val fullySignedTx = subFlow(CollectSignaturesFlow(signedTx, listOf(otherpartySession), CollectSignaturesFlow.tracker()))
|
val fullySignedTx = subFlow(CollectSignaturesFlow(signedTx, listOf(otherPartySession), CollectSignaturesFlow.tracker()))
|
||||||
|
|
||||||
// Finalising the transaction.
|
// Finalising the transaction.
|
||||||
subFlow(FinalityFlow(fullySignedTx))
|
subFlow(FinalityFlow(fullySignedTx))
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
@file:Suppress("unused")
|
||||||
|
|
||||||
package net.corda.docs.kotlin.tutorial.twoparty
|
package net.corda.docs.kotlin.tutorial.twoparty
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package net.corda.docs
|
package net.corda.docs.kotlin
|
||||||
|
|
||||||
import net.corda.core.contracts.Amount
|
import net.corda.core.contracts.Amount
|
||||||
import net.corda.core.contracts.ContractState
|
import net.corda.core.contracts.ContractState
|
||||||
@ -8,7 +8,6 @@ import net.corda.core.node.services.vault.*
|
|||||||
import net.corda.core.utilities.OpaqueBytes
|
import net.corda.core.utilities.OpaqueBytes
|
||||||
import net.corda.core.utilities.getOrThrow
|
import net.corda.core.utilities.getOrThrow
|
||||||
import net.corda.docs.java.tutorial.helloworld.IOUFlow
|
import net.corda.docs.java.tutorial.helloworld.IOUFlow
|
||||||
import net.corda.docs.kotlin.TopupIssuerFlow
|
|
||||||
import net.corda.finance.*
|
import net.corda.finance.*
|
||||||
import net.corda.finance.contracts.getCashBalances
|
import net.corda.finance.contracts.getCashBalances
|
||||||
import net.corda.finance.flows.CashIssueFlow
|
import net.corda.finance.flows.CashIssueFlow
|
@ -1,18 +1,12 @@
|
|||||||
package net.corda.docs
|
package net.corda.docs.kotlin
|
||||||
|
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.toFuture
|
import net.corda.core.toFuture
|
||||||
import net.corda.core.utilities.OpaqueBytes
|
import net.corda.core.utilities.OpaqueBytes
|
||||||
import net.corda.core.utilities.getOrThrow
|
import net.corda.core.utilities.getOrThrow
|
||||||
import net.corda.docs.kotlin.ForeignExchangeFlow
|
import net.corda.finance.*
|
||||||
import net.corda.docs.kotlin.ForeignExchangeRemoteFlow
|
|
||||||
import net.corda.finance.DOLLARS
|
|
||||||
import net.corda.finance.GBP
|
|
||||||
import net.corda.finance.POUNDS
|
|
||||||
import net.corda.finance.USD
|
|
||||||
import net.corda.finance.contracts.getCashBalances
|
import net.corda.finance.contracts.getCashBalances
|
||||||
import net.corda.finance.flows.CashIssueFlow
|
import net.corda.finance.flows.CashIssueFlow
|
||||||
import net.corda.finance.issuedBy
|
|
||||||
import net.corda.testing.core.singleIdentity
|
import net.corda.testing.core.singleIdentity
|
||||||
import net.corda.testing.node.MockNetwork
|
import net.corda.testing.node.MockNetwork
|
||||||
import net.corda.testing.node.StartedMockNode
|
import net.corda.testing.node.StartedMockNode
|
@ -1,4 +1,4 @@
|
|||||||
package net.corda.docs
|
package net.corda.docs.kotlin
|
||||||
|
|
||||||
import net.corda.core.contracts.LinearState
|
import net.corda.core.contracts.LinearState
|
||||||
import net.corda.core.contracts.StateAndRef
|
import net.corda.core.contracts.StateAndRef
|
||||||
@ -9,10 +9,6 @@ import net.corda.core.node.services.queryBy
|
|||||||
import net.corda.core.node.services.vault.QueryCriteria
|
import net.corda.core.node.services.vault.QueryCriteria
|
||||||
import net.corda.core.toFuture
|
import net.corda.core.toFuture
|
||||||
import net.corda.core.utilities.getOrThrow
|
import net.corda.core.utilities.getOrThrow
|
||||||
import net.corda.docs.kotlin.SubmitCompletionFlow
|
|
||||||
import net.corda.docs.kotlin.SubmitTradeApprovalFlow
|
|
||||||
import net.corda.docs.kotlin.TradeApprovalContract
|
|
||||||
import net.corda.docs.kotlin.WorkflowState
|
|
||||||
import net.corda.testing.core.ALICE_NAME
|
import net.corda.testing.core.ALICE_NAME
|
||||||
import net.corda.testing.core.BOB_NAME
|
import net.corda.testing.core.BOB_NAME
|
||||||
import net.corda.testing.node.MockNetwork
|
import net.corda.testing.node.MockNetwork
|
@ -73,7 +73,8 @@ annotation out will lead to some very weird error messages!
|
|||||||
|
|
||||||
There are also a few more annotations, on the ``FlowLogic`` subclass itself:
|
There are also a few more annotations, on the ``FlowLogic`` subclass itself:
|
||||||
|
|
||||||
* ``@InitiatingFlow`` means that this flow can be started directly by the node
|
* ``@InitiatingFlow`` means that this flow is part of a flow pair and that it triggers the other side to run the
|
||||||
|
the counterpart flow.
|
||||||
* ``@StartableByRPC`` allows the node owner to start this flow via an RPC call
|
* ``@StartableByRPC`` allows the node owner to start this flow via an RPC call
|
||||||
|
|
||||||
Let's walk through the steps of ``FlowLogic.call`` itself. This is where we actually describe the procedure for
|
Let's walk through the steps of ``FlowLogic.call`` itself. This is where we actually describe the procedure for
|
||||||
|
@ -1,57 +0,0 @@
|
|||||||
package net.corda.loadtest.tests
|
|
||||||
|
|
||||||
import net.corda.client.mock.Generator
|
|
||||||
import net.corda.core.flows.FinalityFlow
|
|
||||||
import net.corda.core.flows.FlowException
|
|
||||||
import net.corda.core.identity.CordaX500Name
|
|
||||||
import net.corda.core.internal.concurrent.thenMatch
|
|
||||||
import net.corda.core.messaging.startFlow
|
|
||||||
import net.corda.core.transactions.SignedTransaction
|
|
||||||
import net.corda.loadtest.LoadTest
|
|
||||||
import net.corda.loadtest.NodeConnection
|
|
||||||
import net.corda.testing.contracts.DummyContract
|
|
||||||
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
|
||||||
import net.corda.testing.core.TestIdentity
|
|
||||||
import net.corda.testing.node.MockServices
|
|
||||||
import net.corda.testing.node.makeTestIdentityService
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger("NotaryTest")
|
|
||||||
private val dummyCashIssuer = TestIdentity(CordaX500Name("Snake Oil Issuer", "London", "GB"), 10)
|
|
||||||
private val DUMMY_CASH_ISSUER = dummyCashIssuer.ref(1)
|
|
||||||
private val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20)
|
|
||||||
private val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))
|
|
||||||
private val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB"))
|
|
||||||
|
|
||||||
data class NotariseCommand(val issueTx: SignedTransaction, val moveTx: SignedTransaction, val node: NodeConnection)
|
|
||||||
|
|
||||||
val dummyNotarisationTest = LoadTest<NotariseCommand, Unit>(
|
|
||||||
"Notarising dummy transactions",
|
|
||||||
generate = { _, _ ->
|
|
||||||
val issuerServices = MockServices(emptyList(), megaCorp.name, makeTestIdentityService(megaCorp.identity, miniCorp.identity, dummyCashIssuer.identity, dummyNotary.identity), dummyCashIssuer.keyPair)
|
|
||||||
val generateTx = Generator.pickOne(simpleNodes).flatMap { node ->
|
|
||||||
Generator.int().map {
|
|
||||||
val issueBuilder = DummyContract.generateInitial(it, notary.info.legalIdentities[0], DUMMY_CASH_ISSUER) // TODO notary choice
|
|
||||||
val issueTx = issuerServices.signInitialTransaction(issueBuilder)
|
|
||||||
val asset = issueTx.tx.outRef<DummyContract.SingleOwnerState>(0)
|
|
||||||
val moveBuilder = DummyContract.move(asset, dummyCashIssuer.party)
|
|
||||||
val moveTx = issuerServices.signInitialTransaction(moveBuilder)
|
|
||||||
NotariseCommand(issueTx, moveTx, node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Generator.replicate(10, generateTx)
|
|
||||||
},
|
|
||||||
interpret = { _, _ -> },
|
|
||||||
execute = { (issueTx, moveTx, node) ->
|
|
||||||
try {
|
|
||||||
val proxy = node.proxy
|
|
||||||
val issueFlow = proxy.startFlow(::FinalityFlow, issueTx)
|
|
||||||
issueFlow.returnValue.thenMatch({
|
|
||||||
proxy.startFlow(::FinalityFlow, moveTx)
|
|
||||||
}, {})
|
|
||||||
} catch (e: FlowException) {
|
|
||||||
log.error("Failure", e)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
gatherRemoteState = {}
|
|
||||||
)
|
|
Loading…
Reference in New Issue
Block a user