这里将使用https://github.com/ecsoya/spring-fabric-gateway这项目来与fabric网络进行交互。
实现一个积分的合约,合约主要实现以下几个功能点:
- 注册账户,账户地址不可重复。
- 积分转账,转账时要判断双方的地址是否存在,账户积分余额是否足够。
- 积分充值,充值时,充值账户地址必须存在,充值额度必须大于0,否则无意义。
- 积分消费,积分消费时,消费的账户地址必须存在,消费积分额度必须大于0,否则无意义。
- 积分查询
编写智能合约
智能合约将实现上述的几点功能,这里代码比较简单,相信大家也能看懂。
package main
import (
"fmt"
"github.com/hyperledger/fabric/core/chaincode/shim"
pb "github.com/hyperledger/fabric/protos/peer"
"strconv"
)
const (
FunctionRegister = "register"
FunctionTransfer = "transfer"
FunctionQuery = "query"
FunctionConsume = "consume"
FunctionCharge = "charge"
)
type CreditContract struct {
}
// init creditContract
func (t *CreditContract) Init(stub shim.ChaincodeStubInterface) pb.Response {
fmt.Println("CreditContract")
return shim.Success([]byte("init successful"))
}
func (t *CreditContract) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
function, args := stub.GetFunctionAndParameters()
switch function {
case FunctionRegister:
return t.register(stub, args)
case FunctionTransfer:
return t.transfer(stub, args)
case FunctionQuery:
return t.query(stub, args)
case FunctionConsume:
return t.consume(stub, args)
case FunctionCharge:
return t.charge(stub, args)
default:
return shim.Error("Invalid function")
}
}
// register
func (t *CreditContract) register(stub shim.ChaincodeStubInterface, args []string) pb.Response {
if len(args) != 1 {
return shim.Error("Incorrect number of arguments")
}
account := args[0]
// check account exists
bytes, err := stub.GetState(account)
if err != nil && bytes != nil {
return shim.Error("can't register account,the account exists")
}
// register account and init credit as zero
if err := stub.PutState(account, []byte(strconv.Itoa(0))); err != nil {
return shim.Error("register account failed: " + err.Error())
}
return shim.Success([]byte("register account success"))
}
// transfer credit
func (t *CreditContract) transfer(stub shim.ChaincodeStubInterface, args []string) pb.Response {
if len(args) != 3 {
return shim.Error("Incorrect number of arguments")
}
// source account
from := args[0]
// dest account
to := args[1]
// convert int value
creditValue, err := strconv.Atoi(args[2])
if err != nil {
return shim.Error("Invalid param :" + err.Error())
}
// check from account exists
if bytes, err := stub.GetState(from); err != nil || bytes == nil {
return shim.Error("The from account not exists")
}
// check to account exists
if bytes, err := stub.GetState(to); err != nil || bytes == nil {
return shim.Error("The to account not exists")
}
// check from account balance
bytesf, _ := stub.GetState(from)
fromCredit, _ := strconv.Atoi(string(bytesf))
if fromCredit < creditValue {
return shim.Error("Sorry, from account haven't enough credit")
}
// get to account credit
bytest, _ := stub.GetState(to)
toCredit, _ := strconv.Atoi(string(bytest))
// transfer credit
fromCredit = fromCredit - creditValue
toCredit = toCredit + creditValue
stub.PutState(from, []byte(strconv.Itoa(fromCredit)))
stub.PutState(to, []byte(strconv.Itoa(toCredit)))
return shim.Success([]byte("transfer success"))
}
// query credit
func (t *CreditContract) query(stub shim.ChaincodeStubInterface, args []string) pb.Response {
if len(args) != 1 {
return shim.Error("Incorrect number of arguments")
}
bytes, err := stub.GetState(args[0])
if err != nil || bytes == nil {
return shim.Error("query failed")
}
return shim.Success(bytes)
}
// consume credit
func (t *CreditContract) consume(stub shim.ChaincodeStubInterface, args []string) pb.Response {
if len(args) != 2 {
return shim.Error("Incorrect number of arguments")
}
account:=args[0]
credit,err:=strconv.Atoi(args[1])
if err!=nil {
return shim.Error("convert credit failed : "+err.Error())
}
// check account exists
bytes,err:=stub.GetState(account)
if err!=nil||bytes==nil {
return shim.Error("account not exists")
}
balance,err:=strconv.Atoi(string(bytes))
if err!=nil {
return shim.Error("get balance failed: "+err.Error())
}
if balance < credit{
return shim.Error("balance not enough")
}
balance -= credit
if err:=stub.PutState(account,[]byte(strconv.Itoa(balance)));err!=nil{
return shim.Error("consume failed: "+err.Error())
}
return shim.Success([]byte("consume "+args[1] + "credit"))
}
// charge credit
func (t *CreditContract) charge(stub shim.ChaincodeStubInterface, args []string) pb.Response {
if len(args)!=2 {
return shim.Error("Incorrect number of arguments")
}
account:=args[0]
chargeValue,err := strconv.Atoi(args[1])
if err!=nil {
return shim.Error("convert charge failed: "+err.Error())
}
if chargeValue<=0 {
return shim.Error("charge credit should be more than 0")
}
bytes,err:=stub.GetState(account)
if err!=nil||bytes==nil{
return shim.Error("account not exists")
}
balance,err:=strconv.Atoi(string(bytes))
if err!=nil{
return shim.Error("convert balance failed: "+err.Error())
}
balance += chargeValue
if err:=stub.PutState(account,[]byte(strconv.Itoa(balance)));err!=nil{
return shim.Error("charge failed :"+err.Error())
}
return shim.Success([]byte("charge success"))
}
func main() {
err:=shim.Start(new(CreditContract))
if err!=nil{
fmt.Printf("Error starting CreditContract: %s",err)
}
}
SDK交互
创建一个springboot工程,并引入以下依赖包,这个包封装了对fabric的网络操作
<dependency>
<groupId>io.github.ecsoya</groupId>
<artifactId>fabric-gateway-spring-boot-starter</artifactId>
<version>1.0.6</version>
</dependency>
java应用层service实现如下
package com.finefine.fabric.service;
import io.github.ecsoya.fabric.FabricQueryRequest;
import io.github.ecsoya.fabric.FabricQueryResponse;
import io.github.ecsoya.fabric.FabricRequest;
import io.github.ecsoya.fabric.FabricResponse;
import io.github.ecsoya.fabric.config.FabricContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
/**
* @author finefine at: 2020/7/14 3:08 下午
*/
@Service
public class CreditContractServiceImpl implements CreditContractService {
public static final String FUNCTION_REGISTER = "register";
public static final String FUNCTION_QUERY = "query";
public static final String FUNCTION_TRANSFER = "transfer";
public static final String FUNCTION_CONSUME = "consume";
public static final String FUNCTION_CHARGE = "charge";
@Autowired
private FabricContext fabricContext;
@Override
public boolean register(String account) {
Assert.notNull(account, "账户不能为空");
FabricRequest fabricRequest = new FabricRequest(FUNCTION_REGISTER,account);
if (fabricContext.execute(fabricRequest).isOk()){
return true;
}
return false;
}
@Override
public long querybalance(String account) {
Assert.notNull(account, "账户不能为空");
FabricQueryRequest<Long> balanceRequest = new FabricQueryRequest<Long>(Long.class,FUNCTION_QUERY,account);
FabricQueryResponse<Long> balanceResponse = fabricContext.query(balanceRequest);
if (balanceResponse.isOk()){
return balanceResponse.data;
}
throw new RuntimeException(balanceResponse.errorMsg);
}
@Override
public boolean transfer(String from, String to, long credit) {
Assert.notNull(from,"来源地址不能为空");
Assert.notNull(to,"目标地址不能为空");
if (credit<=0){
throw new RuntimeException("转账积分必须大于0");
}
FabricRequest fabricRequest = new FabricRequest(FUNCTION_TRANSFER,from,to,String.valueOf(credit));
FabricResponse fabricResponse = fabricContext.execute(fabricRequest);
if (fabricResponse.isOk()){
return true;
}
throw new RuntimeException(fabricResponse.errorMsg);
}
@Override
public boolean consume(String account, long credit) {
Assert.notNull(account,"账户地址不能为空");
if (credit<=0){
throw new RuntimeException("消费积分不能小于0");
}
FabricRequest fabricRequest = new FabricRequest(FUNCTION_CONSUME,account,String.valueOf(credit));
FabricResponse fabricResponse = fabricContext.execute(fabricRequest);
if (fabricResponse.isOk()){
return true;
}
throw new RuntimeException(fabricResponse.errorMsg);
}
@Override
public boolean charge(String account, long credit) {
Assert.notNull(account,"账户地址不能为空");
if (credit<=0){
throw new RuntimeException("充值积分不能小于0");
}
FabricRequest fabricRequest = new FabricRequest(FUNCTION_CHARGE,account,String.valueOf(credit));
FabricResponse fabricResponse = fabricContext.execute(fabricRequest);
if (fabricResponse.isOk()){
return true;
}
throw new RuntimeException(fabricResponse.errorMsg);
}
}
这里我只实现了service,并使用单元测试进行测试。
package com.finefine.fabric.service;
import org.junit.Assert;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import static org.junit.jupiter.api.Assertions.*;
/**
* @author finefine at: 2020/7/14 3:30 下午
*/
@SpringBootTest
@RunWith(SpringRunner.class)
class CreditContractServiceTest {
@Autowired
private CreditContractService creditContractService;
@Test
void register() {
Assert.assertTrue(creditContractService.register("202007140001"));
Assert.assertTrue(creditContractService.register("202007140002"));
}
@Test
void querybalance() {
Assert.assertEquals(0,creditContractService.querybalance("202007140001"));
Assert.assertEquals(0,creditContractService.querybalance("202007140002"));
}
@Test
void transfer() {
Assert.assertEquals(200l,creditContractService.querybalance("202007140001"));
Assert.assertTrue(creditContractService.transfer("202007140001","202007140002",80));
Assert.assertEquals(120l,creditContractService.querybalance("202007140001"));
Assert.assertEquals(180l,creditContractService.querybalance("202007140002"));
}
@Test
void consume() {
Assert.assertEquals(120l,creditContractService.querybalance("202007140001"));
Assert.assertTrue(creditContractService.consume("202007140001",20));
Assert.assertEquals(100l,creditContractService.querybalance("202007140001"));
}
@Test
void charge() {
Assert.assertTrue(creditContractService.charge("202007140001",200l));
Assert.assertTrue(creditContractService.charge("202007140002",100l));
}
}
运行测试
为了方便测试,修改了first-network项目,使用byfn.sh up 即可快速创建fabric网络和安装编写好的chaincode。
- 进入first-network
./byfn.sh up
- 运行 src 下的 NetworkGenerator 生成fabric网络配置文件。
- 运行单测