package example02;
import org.hyperledger.fabric.contract.Context;
import org.hyperledger.fabric.contract.ContractInterface;
import org.hyperledger.fabric.contract.annotation.*;
import org.hyperledger.fabric.shim.ChaincodeException;
import org.hyperledger.fabric.shim.ChaincodeStub;
/**
* Class: MyContract
*/
@Contract(
name = "example02.MyContract",
info = @Info(
title = "MyContract",
description = "SmartContract Example 02 - Blockchain Workshop",
version = "1.0.0",
license = @License(
name = "Apache 2.0 License",
url = "http://www.apache.org/licenses/LICENSE-2.0.html"),
contact = @Contact(
email = "23227732@qq.com",
name = "Bing"
)
)
)
@Default
public final class MyContract implements ContractInterface {
enum Message {
UNKNOWN_ERROR("chaincode failed with unknown reason."),
FUNC_NOT_SUPPORT("function name '%s' is not support."),
ARG_NUM_WRONG("Incorrect number of arguments. (Expecting %d)"),
ACCOUNT_NOT_EXISTING("Account '%s' does not exist."),
NO_ENOUGH_BALANCE("There is no enough balance to transfer in account '%s'."),
BALANCE_INVALID("Account balance is invalid. ('%s': %s)");
private String tmpl;
private Message(String tmpl) {
this.tmpl = tmpl;
}
public String template() {
return this.tmpl;
}
}
/**
* Initialize Ledger
* @param ctx context
*/
@Transaction(name = "Init", intent = Transaction.TYPE.SUBMIT)
public void init(final Context ctx, final String keyA, final String valueA, final String keyB, final String valueB) {
ChaincodeStub stub = ctx.getStub();
try {
Integer.valueOf(valueA);
} catch (Exception e) {
String errorMessage = String.format(Message.BALANCE_INVALID.template(), keyA, valueA);
System.out.println(errorMessage);
throw new ChaincodeException(errorMessage, e);
}
try {
Integer.valueOf(valueB);
} catch (Exception e) {
String errorMessage = String.format(Message.BALANCE_INVALID.template(), keyB, valueB);
System.out.println(errorMessage);
throw new ChaincodeException(errorMessage, e);
}
// init account A
stub.putStringState(keyA, valueA);
// init account B
stub.putStringState(keyB, valueB);
}
/**
* Query Account
* @param ctx context
* @return name state in ledger
*/
@Transaction(name = "Query", intent = Transaction.TYPE.EVALUATE)
public String query(final Context ctx, final String key) {
ChaincodeStub stub = ctx.getStub();
String valueA = stub.getStringState(key);
// account not existing
if (valueA.isEmpty()) {
String errorMessage = String.format(Message.ACCOUNT_NOT_EXISTING.template(), key);
System.out.println(errorMessage);
throw new ChaincodeException(errorMessage);
}
return valueA;
}
/**
* Transfer Amount
* @param ctx context
*/
@Transaction(name = "Transfer", intent = Transaction.TYPE.SUBMIT)
public void transfer(final Context ctx, final String keyFrom, final String keyTo, final String valueTrans) {
ChaincodeStub stub = ctx.getStub();
String valueA = stub.getStringState(keyFrom);
String valueB = stub.getStringState(keyTo);
int intValueA = Integer.parseInt(valueA);
int intValueB = Integer.parseInt(valueB);
int intValueTrans = Integer.parseInt(valueTrans);
if (intValueA < intValueTrans) {
String errorMessage = String.format(Message.NO_ENOUGH_BALANCE.template(), keyFrom);
throw new ChaincodeException(errorMessage);
}
intValueA = intValueA - intValueTrans;
stub.putStringState(keyFrom, String.valueOf(intValueA));
intValueB = intValueB + intValueTrans;
stub.putStringState(keyTo, String.valueOf(intValueB));
}
@Transaction(name = "Charge", intent = Transaction.TYPE.SUBMIT)
public void charge(final Context ctx, final String key, final String chargeValue){
ChaincodeStub stub = ctx.getStub();
String value = stub.getStringState(key);
// account not existing
if (value.isEmpty()) {
String errorMessage = String.format(Message.ACCOUNT_NOT_EXISTING.template(), key);
System.out.println(errorMessage);
throw new ChaincodeException(errorMessage);
}
int intValue = Integer.parseInt(value);
// value not valid
int intChargeValue = Integer.parseInt(chargeValue);
if (intChargeValue<0){
String errorMessage = String.format(Message.ARG_NUM_WRONG.template(), intChargeValue);
throw new ChaincodeException(errorMessage);
}
intValue = intValue + intChargeValue;
stub.putStringState(key, String.valueOf(intValue));
}
}
test:
package example02;
import org.hyperledger.fabric.contract.Context;
import org.hyperledger.fabric.shim.ChaincodeException;
import org.hyperledger.fabric.shim.ChaincodeStub;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.ThrowableAssert.catchThrowable;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import org.mockito.InOrder;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
public class MyContractTest {
@Nested
class InvokeInitTransaction {
@Test
public void initLedger() {
MyContract contract = new MyContract();
Context ctx = mock(Context.class);
ChaincodeStub stub = mock(ChaincodeStub.class);
when(ctx.getStub()).thenReturn(stub);
contract.init(ctx, "A", "100", "B", "200");
InOrder inOrder = inOrder(stub);
inOrder.verify(stub).putStringState("A", "100");
inOrder.verify(stub).putStringState("B", "200");
}
@Test
public void whenBalanceANotValid() {
MyContract contract = new MyContract();
Context ctx = mock(Context.class);
ChaincodeStub stub = mock(ChaincodeStub.class);
when(ctx.getStub()).thenReturn(stub);
Throwable thrown = catchThrowable(() -> {
contract.init(ctx, "A", "100A", "B", "200");
});
assertThat(thrown)
.isInstanceOf(ChaincodeException.class)
.hasMessage(String.format(MyContract.Message.BALANCE_INVALID.template(), "A", "100A"));
}
}
@Nested
class InvokeTransferTransaction {
@Test
public void whenBalanceEnough() {
MyContract contract = new MyContract();
Context ctx = mock(Context.class);
ChaincodeStub stub = mock(ChaincodeStub.class);
when(ctx.getStub()).thenReturn(stub);
when(stub.getStringState("A")).thenReturn("100");
when(stub.getStringState("B")).thenReturn("100");
Throwable thrown = catchThrowable(() -> {
contract.transfer(ctx, "A", "B", "15");
});
assertThat(thrown).isNull();
}
@Test
public void whenBalanceNotEnough() {
MyContract contract = new MyContract();
Context ctx = mock(Context.class);
ChaincodeStub stub = mock(ChaincodeStub.class);
when(ctx.getStub()).thenReturn(stub);
when(stub.getStringState("A")).thenReturn("100");
when(stub.getStringState("B")).thenReturn("100");
Throwable thrown = catchThrowable(() -> {
contract.transfer(ctx, "A", "B", "150");
});
assertThat(thrown)
.isInstanceOf(ChaincodeException.class)
.hasNoCause()
.hasMessage(String.format(MyContract.Message.NO_ENOUGH_BALANCE.template(), "A"));
}
}
@Nested
class InvokeQueryTransaction {
@Test
public void whenAccountNotExists() {
MyContract contract = new MyContract();
Context ctx = mock(Context.class);
ChaincodeStub stub = mock(ChaincodeStub.class);
when(ctx.getStub()).thenReturn(stub);
when(stub.getStringState("A")).thenReturn("");
Throwable thrown = catchThrowable(() -> {
contract.query(ctx, "A");
});
assertThat(thrown)
.isInstanceOf(ChaincodeException.class)
.hasNoCause()
.hasMessage(String.format(MyContract.Message.ACCOUNT_NOT_EXISTING.template(), "A"));
}
@Test
public void whenAccountExists() {
MyContract contract = new MyContract();
Context ctx = mock(Context.class);
ChaincodeStub stub = mock(ChaincodeStub.class);
when(ctx.getStub()).thenReturn(stub);
when(stub.getStringState("A")).thenReturn("100");
assertThat(contract.query(ctx, "A")).isEqualTo("100");
}
}
@Nested
class InvokeChargeTransaction {
@Test
public void whenAccountNotExists() {
MyContract contract = new MyContract();
Context ctx = mock(Context.class);
ChaincodeStub stub = mock(ChaincodeStub.class);
when(ctx.getStub()).thenReturn(stub);
when(stub.getStringState("A")).thenReturn("");
Throwable thrown = catchThrowable(() -> {
contract.charge(ctx,"A","10");
});
assertThat(thrown)
.isInstanceOf(ChaincodeException.class)
.hasNoCause()
.hasMessage(String.format(MyContract.Message.ACCOUNT_NOT_EXISTING.template(), "A"));
}
@Test
public void whenValueNotValid() {
MyContract contract = new MyContract();
Context ctx = mock(Context.class);
ChaincodeStub stub = mock(ChaincodeStub.class);
when(ctx.getStub()).thenReturn(stub);
when(stub.getStringState("A")).thenReturn("100");
Throwable thrown = catchThrowable(() -> {
contract.charge(ctx,"A","-10");
});
assertThat(thrown)
.isInstanceOf(ChaincodeException.class)
.hasNoCause()
.hasMessage(String.format(MyContract.Message.ARG_NUM_WRONG.template(), -10));
}
@Test
public void whenAccountExistsAndValueValid() {
MyContract contract = new MyContract();
Context ctx = mock(Context.class);
ChaincodeStub stub = mock(ChaincodeStub.class);
when(ctx.getStub()).thenReturn(stub);
when(stub.getStringState("A")).thenReturn("100");
Throwable thrown = catchThrowable(() -> {
contract.charge(ctx,"A","10");
});
assertThat(thrown).isNull();
}
}
}