Fabric实战《SDK使用及智能合约开发》

496 阅读4分钟

项目地址:github.com/tianrandail…

这里将使用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。

  1. 进入first-network
./byfn.sh up
  1. 运行 src 下的 NetworkGenerator 生成fabric网络配置文件。
  2. 运行单测