mirror of
https://github.com/corda/corda.git
synced 2024-12-19 04:57:58 +00:00
Merged in rg_work_in_progress (pull request #16)
Updated as comments per previous pull request, now generic tests run both Kotlin and Java CommercialPaper class tests.
This commit is contained in:
commit
ea2b18eb41
25
src/main/java/contracts/ICommercialPaperState.java
Normal file
25
src/main/java/contracts/ICommercialPaperState.java
Normal file
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2015 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members
|
||||
* pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms
|
||||
* set forth therein.
|
||||
*
|
||||
* All other rights reserved.
|
||||
*/
|
||||
package contracts;
|
||||
|
||||
import core.*;
|
||||
|
||||
import java.security.*;
|
||||
import java.time.*;
|
||||
|
||||
/* This is an interface solely created to demonstrate that the same kotlin tests can be run against
|
||||
* either a Java implementation of the CommercialPaper or a kotlin implementation.
|
||||
* Normally one would not duplicate an implementation in different languages for obvious reasons, but it demonstrates that
|
||||
* ultimately either language can be used against a common test framework (and therefore can be used for real).
|
||||
*/
|
||||
public interface ICommercialPaperState extends ContractState {
|
||||
ICommercialPaperState withOwner(PublicKey newOwner);
|
||||
ICommercialPaperState withIssuance(PartyReference newIssuance);
|
||||
ICommercialPaperState withFaceValue(Amount newFaceValue);
|
||||
ICommercialPaperState withMaturityDate(Instant newMaturityDate);
|
||||
}
|
@ -47,12 +47,18 @@ class CommercialPaper : Contract {
|
||||
override val owner: PublicKey,
|
||||
val faceValue: Amount,
|
||||
val maturityDate: Instant
|
||||
) : OwnableState {
|
||||
) : OwnableState, ICommercialPaperState {
|
||||
override val programRef = CP_PROGRAM_ID
|
||||
|
||||
fun withoutOwner() = copy(owner = NullPublicKey)
|
||||
override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner))
|
||||
override fun toString() = "${Emoji.newspaper}CommercialPaper(of $faceValue redeemable on $maturityDate by '$issuance', owned by ${owner.toStringShort()})"
|
||||
|
||||
// Although kotlin is smart enough not to need these, as we are using the ICommercialPaperState, we need to declare them explicitly for use later,
|
||||
override fun withOwner(newOwner: PublicKey): ICommercialPaperState = copy(owner = newOwner)
|
||||
override fun withIssuance(newIssuance: PartyReference): ICommercialPaperState = copy(issuance = newIssuance)
|
||||
override fun withFaceValue(newFaceValue: Amount): ICommercialPaperState = copy(faceValue = newFaceValue)
|
||||
override fun withMaturityDate(newMaturityDate: Instant): ICommercialPaperState = copy(maturityDate = newMaturityDate)
|
||||
}
|
||||
|
||||
interface Commands : CommandData {
|
||||
|
@ -19,14 +19,16 @@ import java.util.*;
|
||||
import static core.ContractsDSLKt.*;
|
||||
import static kotlin.collections.CollectionsKt.*;
|
||||
|
||||
|
||||
/**
|
||||
* This is a Java version of the CommercialPaper contract (chosen because it's simple). This demonstrates how the
|
||||
* use of Kotlin for implementation of the framework does not impose the same language choice on contract developers.
|
||||
*
|
||||
* NOTE: For illustration only. Not unit tested.
|
||||
*/
|
||||
public class JavaCommercialPaper implements Contract {
|
||||
public static class State implements ContractState {
|
||||
public static core.SecureHash JCP_PROGRAM_ID = SecureHash.Companion.sha256("java commercial paper (this should be a bytecode hash)");
|
||||
|
||||
public static class State implements ContractState, ICommercialPaperState {
|
||||
private PartyReference issuance;
|
||||
private PublicKey owner;
|
||||
private Amount faceValue;
|
||||
@ -42,8 +44,23 @@ public class JavaCommercialPaper implements Contract {
|
||||
}
|
||||
|
||||
public State copy() {
|
||||
State ret = new State(this.issuance, this.owner, this.faceValue, this.maturityDate);
|
||||
return ret;
|
||||
return new State(this.issuance, this.owner, this.faceValue, this.maturityDate);
|
||||
}
|
||||
|
||||
public ICommercialPaperState withOwner(PublicKey newOwner) {
|
||||
return new State(this.issuance, newOwner, this.faceValue, this.maturityDate);
|
||||
}
|
||||
|
||||
public ICommercialPaperState withIssuance(PartyReference newIssuance) {
|
||||
return new State(newIssuance, this.owner, this.faceValue, this.maturityDate);
|
||||
}
|
||||
|
||||
public ICommercialPaperState withFaceValue(Amount newFaceValue) {
|
||||
return new State(this.issuance, this.owner, newFaceValue, this.maturityDate);
|
||||
}
|
||||
|
||||
public ICommercialPaperState withMaturityDate(Instant newMaturityDate) {
|
||||
return new State(this.issuance, this.owner, this.faceValue, newMaturityDate);
|
||||
}
|
||||
|
||||
public PartyReference getIssuance() {
|
||||
@ -120,46 +137,78 @@ public class JavaCommercialPaper implements Contract {
|
||||
|
||||
@Override
|
||||
public void verify(@NotNull TransactionForVerification tx) {
|
||||
// There are two possible things that can be done with CP. The first is trading it. The second is redeeming it
|
||||
// for cash on or after the maturity date.
|
||||
// There are three possible things that can be done with CP.
|
||||
// Issuance, trading (aka moving in this prototype) and redeeming.
|
||||
// Each command has it's own set of restrictions which the verify function ... verifies.
|
||||
|
||||
List<InOutGroup<State>> groups = tx.groupStates(State.class, State::withoutOwner);
|
||||
|
||||
// Find the command that instructs us what to do and check there's exactly one.
|
||||
AuthenticatedObject<CommandData> cmd = requireSingleCommand(tx.getCommands(), Commands.class);
|
||||
|
||||
TimestampCommand timestampCommand = tx.getTimestampBy(DummyTimestampingAuthority.INSTANCE.getIdentity());
|
||||
if (timestampCommand == null)
|
||||
throw new IllegalArgumentException("must be timestamped");
|
||||
Instant time = timestampCommand.getMidpoint();
|
||||
AuthenticatedObject<CommandData> cmd = requireSingleCommand(tx.getCommands(), JavaCommercialPaper.Commands.class);
|
||||
|
||||
for (InOutGroup<State> group : groups) {
|
||||
List<State> inputs = group.getInputs();
|
||||
List<State> outputs = group.getOutputs();
|
||||
|
||||
// For now do not allow multiple pieces of CP to trade in a single transaction. Study this more!
|
||||
State input = single(filterIsInstance(inputs, State.class));
|
||||
|
||||
if (!cmd.getSigners().contains(input.getOwner()))
|
||||
throw new IllegalStateException("Failed requirement: the transaction is signed by the owner of the CP");
|
||||
|
||||
if (cmd.getValue() instanceof JavaCommercialPaper.Commands.Move) {
|
||||
// Check the output CP state is the same as the input state, ignoring the owner field.
|
||||
// For now do not allow multiple pieces of CP to trade in a single transaction.
|
||||
if (cmd.getValue() instanceof JavaCommercialPaper.Commands.Issue) {
|
||||
State output = single(outputs);
|
||||
if (!inputs.isEmpty()) {
|
||||
throw new IllegalStateException("Failed Requirement: there is no input state");
|
||||
}
|
||||
if (output.faceValue.getPennies() == 0) {
|
||||
throw new IllegalStateException("Failed Requirement: the face value is not zero");
|
||||
}
|
||||
|
||||
if (!output.getFaceValue().equals(input.getFaceValue()) ||
|
||||
!output.getIssuance().equals(input.getIssuance()) ||
|
||||
!output.getMaturityDate().equals(input.getMaturityDate()))
|
||||
throw new IllegalStateException("Failed requirement: the output state is the same as the input state except for owner");
|
||||
} else if (cmd.getValue() instanceof JavaCommercialPaper.Commands.Redeem) {
|
||||
Amount received = CashKt.sumCashOrNull(inputs);
|
||||
if (received == null)
|
||||
throw new IllegalStateException("Failed requirement: no cash being redeemed");
|
||||
if (input.getMaturityDate().isAfter(time))
|
||||
throw new IllegalStateException("Failed requirement: the paper must have matured");
|
||||
if (!input.getFaceValue().equals(received))
|
||||
throw new IllegalStateException("Failed requirement: the received amount equals the face value");
|
||||
if (!outputs.isEmpty())
|
||||
throw new IllegalStateException("Failed requirement: the paper must be destroyed");
|
||||
TimestampCommand timestampCommand = tx.getTimestampBy(DummyTimestampingAuthority.INSTANCE.getIdentity());
|
||||
if (timestampCommand == null)
|
||||
throw new IllegalArgumentException("Failed Requirement: must be timestamped");
|
||||
|
||||
Instant time = timestampCommand.getBefore();
|
||||
|
||||
if (! time.isBefore(output.maturityDate)) {
|
||||
throw new IllegalStateException("Failed Requirement: the maturity date is not in the past");
|
||||
}
|
||||
|
||||
if (!cmd.getSigners().contains(output.issuance.getParty().getOwningKey())) {
|
||||
throw new IllegalStateException("Failed Requirement: the issuance is signed by the claimed issuer of the paper");
|
||||
}
|
||||
}
|
||||
else { // Everything else (Move, Redeem) requires inputs (they are not first to be actioned)
|
||||
// There should be only a single input due to aggregation above
|
||||
State input = single(inputs);
|
||||
|
||||
if (!cmd.getSigners().contains(input.getOwner()))
|
||||
throw new IllegalStateException("Failed requirement: the transaction is signed by the owner of the CP");
|
||||
|
||||
if (cmd.getValue() instanceof JavaCommercialPaper.Commands.Move) {
|
||||
// Check the output CP state is the same as the input state, ignoring the owner field.
|
||||
State output = single(outputs);
|
||||
|
||||
if (!output.getFaceValue().equals(input.getFaceValue()) ||
|
||||
!output.getIssuance().equals(input.getIssuance()) ||
|
||||
!output.getMaturityDate().equals(input.getMaturityDate()))
|
||||
throw new IllegalStateException("Failed requirement: the output state is the same as the input state except for owner");
|
||||
}
|
||||
else if (cmd.getValue() instanceof JavaCommercialPaper.Commands.Redeem)
|
||||
{
|
||||
TimestampCommand timestampCommand = tx.getTimestampBy(DummyTimestampingAuthority.INSTANCE.getIdentity());
|
||||
if (timestampCommand == null)
|
||||
throw new IllegalArgumentException("Failed Requirement: must be timestamped");
|
||||
Instant time = timestampCommand.getBefore();
|
||||
|
||||
Amount received = CashKt.sumCashBy(tx.getOutStates(), input.getOwner());
|
||||
|
||||
if (! received.equals(input.getFaceValue()))
|
||||
throw new IllegalStateException(String.format("Failed Requirement: received amount equals the face value"));
|
||||
if (time.isBefore(input.getMaturityDate()))
|
||||
throw new IllegalStateException("Failed requirement: the paper must have matured");
|
||||
if (!input.getFaceValue().equals(received))
|
||||
throw new IllegalStateException("Failed requirement: the received amount equals the face value");
|
||||
if (!outputs.isEmpty())
|
||||
throw new IllegalStateException("Failed requirement: the paper must be destroyed");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -170,4 +219,22 @@ public class JavaCommercialPaper implements Contract {
|
||||
// TODO: Should return hash of the contract's contents, not its URI
|
||||
return SecureHash.Companion.sha256("https://en.wikipedia.org/wiki/Commercial_paper");
|
||||
}
|
||||
|
||||
public TransactionBuilder craftIssue(@NotNull PartyReference issuance, @NotNull Amount faceValue, @Nullable Instant maturityDate) {
|
||||
State state = new State(issuance,issuance.getParty().getOwningKey(), faceValue, maturityDate);
|
||||
return new TransactionBuilder().withItems(state, new Command( new Commands.Issue(), issuance.getParty().getOwningKey()));
|
||||
}
|
||||
|
||||
public void craftRedeem(TransactionBuilder tx, StateAndRef<State> paper, List<StateAndRef<Cash.State>> wallet) throws InsufficientBalanceException {
|
||||
new Cash().craftSpend(tx, paper.getState().getFaceValue(), paper.getState().getOwner(), wallet, null);
|
||||
tx.addInputState(paper.getRef());
|
||||
tx.addCommand(new Command( new Commands.Redeem(), paper.getState().getOwner()));
|
||||
}
|
||||
|
||||
public void craftMove(TransactionBuilder tx, StateAndRef<State> paper, PublicKey newOwner) {
|
||||
tx.addInputState(paper.getRef());
|
||||
tx.addOutputState(new State(paper.getState().getIssuance(), newOwner, paper.getState().getFaceValue(), paper.getState().getMaturityDate()));
|
||||
tx.addCommand(new Command(new Commands.Move(), paper.getState().getOwner()));
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -18,13 +18,17 @@ import java.time.ZoneOffset
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class CommercialPaperTests {
|
||||
val PAPER_1 = CommercialPaper.State(
|
||||
issuance = MEGA_CORP.ref(123),
|
||||
owner = MEGA_CORP_PUBKEY,
|
||||
faceValue = 1000.DOLLARS,
|
||||
maturityDate = TEST_TX_TIME + 7.days
|
||||
)
|
||||
interface ICommercialPaperTestTemplate {
|
||||
open fun getPaper() : ICommercialPaperState
|
||||
open fun getIssueCommand() : CommandData
|
||||
open fun getRedeemCommand() : CommandData
|
||||
open fun getMoveCommand() : CommandData
|
||||
}
|
||||
|
||||
open class CommercialPaperTestsGeneric(templateToTest: ICommercialPaperTestTemplate) {
|
||||
|
||||
val thisTest = templateToTest
|
||||
val PAPER_1 = thisTest.getPaper()
|
||||
|
||||
@Test
|
||||
fun ok() {
|
||||
@ -41,7 +45,7 @@ class CommercialPaperTests {
|
||||
transactionGroup {
|
||||
transaction {
|
||||
output { PAPER_1 }
|
||||
arg(DUMMY_PUBKEY_1) { CommercialPaper.Commands.Issue() }
|
||||
arg(DUMMY_PUBKEY_1) { thisTest.getIssueCommand() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
@ -53,8 +57,8 @@ class CommercialPaperTests {
|
||||
fun `face value is not zero`() {
|
||||
transactionGroup {
|
||||
transaction {
|
||||
output { PAPER_1.copy(faceValue = 0.DOLLARS) }
|
||||
arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
|
||||
output { PAPER_1.withFaceValue(0.DOLLARS) }
|
||||
arg(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
@ -66,8 +70,8 @@ class CommercialPaperTests {
|
||||
fun `maturity date not in the past`() {
|
||||
transactionGroup {
|
||||
transaction {
|
||||
output { PAPER_1.copy(maturityDate = TEST_TX_TIME - 10.days) }
|
||||
arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
|
||||
output { PAPER_1.withMaturityDate(TEST_TX_TIME - 10.days) }
|
||||
arg(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
@ -106,7 +110,7 @@ class CommercialPaperTests {
|
||||
transaction {
|
||||
input("paper")
|
||||
output { PAPER_1 }
|
||||
arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
|
||||
arg(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
@ -189,7 +193,7 @@ class CommercialPaperTests {
|
||||
// Generate a trade lifecycle with various parameters.
|
||||
fun trade(redemptionTime: Instant = TEST_TX_TIME + 8.days,
|
||||
aliceGetsBack: Amount = 1000.DOLLARS,
|
||||
destroyPaperAtRedemption: Boolean = true): TransactionGroupDSL<CommercialPaper.State> {
|
||||
destroyPaperAtRedemption: Boolean = true): TransactionGroupDSL<ICommercialPaperState> {
|
||||
val someProfits = 1200.DOLLARS
|
||||
return transactionGroupFor() {
|
||||
roots {
|
||||
@ -200,7 +204,7 @@ class CommercialPaperTests {
|
||||
// Some CP is issued onto the ledger by MegaCorp.
|
||||
transaction("Issuance") {
|
||||
output("paper") { PAPER_1 }
|
||||
arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
|
||||
arg(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
}
|
||||
|
||||
@ -212,7 +216,7 @@ class CommercialPaperTests {
|
||||
output("borrowed $900") { 900.DOLLARS.CASH `owned by` MEGA_CORP_PUBKEY }
|
||||
output("alice's paper") { "paper".output `owned by` ALICE }
|
||||
arg(ALICE) { Cash.Commands.Move() }
|
||||
arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
|
||||
arg(MEGA_CORP_PUBKEY) { thisTest.getMoveCommand() }
|
||||
}
|
||||
|
||||
// Time passes, and Alice redeem's her CP for $1000, netting a $100 profit. MegaCorp has received $1200
|
||||
@ -227,14 +231,10 @@ class CommercialPaperTests {
|
||||
output { "paper".output }
|
||||
|
||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
arg(ALICE) { CommercialPaper.Commands.Redeem() }
|
||||
arg(ALICE) { thisTest.getRedeemCommand() }
|
||||
|
||||
timestamp(redemptionTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
CommercialPaperTests().trade().visualise()
|
||||
}
|
39
src/test/kotlin/contracts/CommercialPaperTestsJava.kt
Normal file
39
src/test/kotlin/contracts/CommercialPaperTestsJava.kt
Normal file
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2015 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members
|
||||
* pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms
|
||||
* set forth therein.
|
||||
*
|
||||
* All other rights reserved.
|
||||
*/
|
||||
|
||||
package contracts
|
||||
|
||||
import core.CommandData
|
||||
import core.DOLLARS
|
||||
import core.days
|
||||
import core.testutils.MEGA_CORP
|
||||
import core.testutils.MEGA_CORP_PUBKEY
|
||||
import core.testutils.TEST_TX_TIME
|
||||
|
||||
fun getJavaCommericalPaper() : ICommercialPaperState {
|
||||
return JavaCommercialPaper.State(
|
||||
MEGA_CORP.ref(123),
|
||||
MEGA_CORP_PUBKEY,
|
||||
1000.DOLLARS,
|
||||
TEST_TX_TIME + 7.days
|
||||
)
|
||||
}
|
||||
|
||||
open class JavaCommercialPaperTest() : ICommercialPaperTestTemplate {
|
||||
override fun getPaper() : ICommercialPaperState = getJavaCommericalPaper()
|
||||
override fun getIssueCommand() : CommandData = JavaCommercialPaper.Commands.Issue()
|
||||
override fun getRedeemCommand() : CommandData = JavaCommercialPaper.Commands.Redeem()
|
||||
override fun getMoveCommand() : CommandData = JavaCommercialPaper.Commands.Move()
|
||||
}
|
||||
|
||||
class CommercialPaperTestsJava() : CommercialPaperTestsGeneric(JavaCommercialPaperTest()) { }
|
||||
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
CommercialPaperTestsJava().trade().visualise()
|
||||
}
|
39
src/test/kotlin/contracts/CommercialPaperTestsKotlin.kt
Normal file
39
src/test/kotlin/contracts/CommercialPaperTestsKotlin.kt
Normal file
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2015 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members
|
||||
* pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms
|
||||
* set forth therein.
|
||||
*
|
||||
* All other rights reserved.
|
||||
*/
|
||||
|
||||
package contracts
|
||||
|
||||
import core.CommandData
|
||||
import core.DOLLARS
|
||||
import core.days
|
||||
import core.testutils.MEGA_CORP
|
||||
import core.testutils.MEGA_CORP_PUBKEY
|
||||
import core.testutils.TEST_TX_TIME
|
||||
|
||||
fun getKotlinCommercialPaper() : ICommercialPaperState {
|
||||
return CommercialPaper.State(
|
||||
issuance = MEGA_CORP.ref(123),
|
||||
owner = MEGA_CORP_PUBKEY,
|
||||
faceValue = 1000.DOLLARS,
|
||||
maturityDate = TEST_TX_TIME + 7.days
|
||||
)
|
||||
}
|
||||
|
||||
open class KotlinCommercialPaperTest() : ICommercialPaperTestTemplate {
|
||||
override fun getPaper() : ICommercialPaperState = getKotlinCommercialPaper()
|
||||
override fun getIssueCommand() : CommandData = CommercialPaper.Commands.Issue()
|
||||
override fun getRedeemCommand() : CommandData = CommercialPaper.Commands.Redeem()
|
||||
override fun getMoveCommand() : CommandData = CommercialPaper.Commands.Move()
|
||||
}
|
||||
|
||||
class CommercialPaperTestsKotlin() : CommercialPaperTestsGeneric( KotlinCommercialPaperTest()) { }
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
CommercialPaperTestsKotlin().trade().visualise()
|
||||
}
|
||||
|
@ -53,6 +53,7 @@ val TEST_TX_TIME = Instant.parse("2015-04-17T12:00:00.00Z")
|
||||
val TEST_PROGRAM_MAP: Map<SecureHash, Contract> = mapOf(
|
||||
CASH_PROGRAM_ID to Cash(),
|
||||
CP_PROGRAM_ID to CommercialPaper(),
|
||||
JavaCommercialPaper.JCP_PROGRAM_ID to JavaCommercialPaper(),
|
||||
CROWDFUND_PROGRAM_ID to CrowdFund(),
|
||||
DUMMY_PROGRAM_ID to DummyContract
|
||||
)
|
||||
@ -79,7 +80,8 @@ val TEST_PROGRAM_MAP: Map<SecureHash, Contract> = mapOf(
|
||||
// TODO: Make it impossible to forget to test either a failure or an accept for each transaction{} block
|
||||
|
||||
infix fun Cash.State.`owned by`(owner: PublicKey) = this.copy(owner = owner)
|
||||
infix fun CommercialPaper.State.`owned by`(owner: PublicKey) = this.copy(owner = owner)
|
||||
infix fun ICommercialPaperState.`owned by`(new_owner: PublicKey) = this.withOwner(new_owner)
|
||||
|
||||
// Allows you to write 100.DOLLARS.CASH
|
||||
val Amount.CASH: Cash.State get() = Cash.State(MINI_CORP.ref(1,2,3), this, NullPublicKey)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user