基于XuperChain区块链的商品溯源防伪系统,包含前后端 https://anti.shellmiao.com/

lutingsong 7fd59ad07c chore readme преди 2 години
anti-fake 7d3caf3dcb add front end and gitignore преди 2 години
anti-fake-back-end 7fd59ad07c chore readme преди 2 години
.DS_Store 9c3bd303bc chore readme преди 2 години
.gitignore 7d3caf3dcb add front end and gitignore преди 2 години
README.md 3684aa5cfb first commit преди 2 години

README.md

基于 XuperChain 框架的区块链商品防伪系统

展示地址:https://anti.shellmiao.com/ 使用需要使用Admin助记词和合约账户作为身份认证 这里可用测试账号:

  • 助记词:铝 纵 殿 熙 何 云 关 棒 悟 信 析 造
  • 合约账号:XC0751444561687239@xuper

什么是区块链

区块链技术的核心概念是分布式账本,区块链网络中的每个节点对 账本/数据/状态 都存有相同的副本,因此想要篡改 账本/数据/状态 的成本是极其高的。改变记录的唯一方法是让大多数节点同意这样做。

什么是智能合约

以太坊(区块链2.0)和比特币(区块链1.0)的不同是以太坊具有可编程特性,添加了对智能合约的支持。而智能合约则是可以在区块链上运行的代码

智能合约是区块链网络上的一种特殊账户

  • 用户账户
    • 地址
    • 余额
  • 智能合约账户
    • 地址
    • 余额
    • 代码
    • 状态

智能合约中的状态是智能合约中声明的所有变量和变量的当前状态,这些变量会一直存在于区块链网络中并依赖区块链网络保证其不可篡改性

智能合约中的代码是编译后可以在区块链网络上运行的字节码

创建和调用智能合约作为交易的一种形式被区块链记录

以太坊和比特币在交易层面的区别

  • 价值传递 (和比特币相同)
  • TO :收款地址
  • DATA :留空或留言信息
  • FROM :谁发出
  • AMOUNT :发送多少
  • 创建合约
  • TO :留空 (触发创建智能合约)
  • DATA :包含编译为字节码的智能合约代码
  • FROM :谁创建
  • AMOUNT :可以是零或任何数量的以太,它是我们想要给合约的存款
  • 调用合约函数
  • TO: 目标合约账户地址
  • DATA: 包含函数名称和参数 - 标识如何调用智能合约函数
  • FROM :谁调用
  • AMOUNT : 可以是零或任意数量的以太,例如可以支付合约服务费用

智能合约的工作流程

构建合约

多方用户共同参与制定一份智能合约,其中明确规定了交易双方的权利和义务,开发人员将这些权利和义务以电子化的方式进行编程,代码中包含会触发合约自动执行的条件

部署合约

合约通过P2P网络扩散并存入区块链,全网的各个节点都可以接收到这份合约

调用合约

智能合约会定期检查是否存在相关事件和触发条件,将满足条件的事件推送到待验证的队列中,区块链上的验证节点先对事件进行签名认证,以确保其有效性,等大多数验证节点对该事件达成共识,智能合约将成功执行,并通知用户

注意:合约不能主动执行,它只能被外部账户发起调用。如果一个合约要定期执行,那只能由线下服务器定期发起合约调用

设计思路

溯源,就是对商品生产、销售的各个环节进行记录,在传统记录条件下,记录容易被篡改,例如生产商内部人员恶意在数据库中新增不能存在的商品数据,并卖给盗版厂商,导致盗版商品难以被鉴别。

但是在区块链环境下,恶意篡改区块链字段的成本极为高昂,可以认为几乎是不可能的。因此我们需要将商品的溯源信息部署在区块链上。这里有两种思路:

  1. 直接将溯源信息作为智能合约的一个变量上链,但是对溯源信息的要求是不能过于复杂
  2. 将溯源信息进行存储,对溯源信息的每一次改变执行hash,将hash上链,保证信息不被篡改

这里我们使用第一种,即创建一个字段,这个字段描述了一个商品从被创建开始的整个生产销售链路,商品信息可以被任何人查询,但是只能由被授权的账号新建或者修改(上下游企业)

于是可以得到下列函数设计

		/*
     * func: 添加admin账户
     * @param: admin: 哪个address具有商品管理员权限
     */
    virtual void addAdmin() = 0;
    /*
     * func: 创建一个新的商品
     * 说明: 仅有合约发起者为admin时才允许创建商品
     * @param: id: 商品id
     * @param: desc: 商品描述
     */
    virtual void createGoods() = 0;
    /*
     * func: 变更商品信息
     * 说明: 仅有合约发起者为admin时才允许变更商品
     * @param: id: 商品id
     * @param: reason: 变更原因
     */
    virtual void updateGoods() = 0;
    /*
     * func: 查询商品变更记录
     * @param: id: 商品id
     */
    virtual void queryRecords() = 0;

智能合约开发环境

XuperIDE
Xuperchain Node in Docker 3.10.0
Xdev in Docker 3.10.0
Xindexer in Docker 1.0.0

XuperIDE使用文档

开发商品溯源智能合约

官方有各种合约模版可供参考和魔改

C++合约模版

Go合约模版

Java合约模版

// 在我使用Go合约模版时遇到的问题
// 注意导入地址使用这两个,官方模版中的地址已经不能用了
"github.com/xuperchain/xuperchain/core/contractsdk/go/code"
"github.com/xuperchain/xuperchain/core/contractsdk/go/driver"
// 下面这两个已经不能用了
"github.com/xuperchain/contract-sdk-go/code"
"github.com/xuperchain/contract-sdk-go/driver"

也可以在控制台在线创建智能合约并使用在线IDE编写合约

image-20220604231513355

image-20220604231540506

这里使用了商品溯源C++合约模板

#include "xchain/xchain.h"


// 商品溯源合约模板
// 参数由xchain::Contract中的context提供
class SourceTrace {
public:
    /*
     * func: 初始化商品溯源合约
     * @param: admin: 哪个address具有商品管理员权限
     */
    virtual void initialize() = 0;
    /*
     * func: 添加admin账户
     * @param: admin: 哪个address具有商品管理员权限
     */
    virtual void addAdmin() = 0;
    /*
     * func: 创建一个新的商品
     * 说明: 仅有合约发起者为admin时才允许创建商品
     * @param: id: 商品id
     * @param: desc: 商品描述
     */
    virtual void createGoods() = 0;
    /*
     * func: 变更商品信息
     * 说明: 仅有合约发起者为admin时才允许变更商品
     * @param: id: 商品id
     * @param: reason: 变更原因
     */
    virtual void updateGoods() = 0;
    /*
     * func: 查询商品变更记录
     * @param: id: 商品id
     */
    virtual void queryRecords() = 0;

};

struct SourceTraceDemo : public SourceTrace, public xchain::Contract {
public:
    const std::string GOODS = "GOODS_";
    const std::string GOODSRECORD = "GOODSSRECORD_";
    const std::string GOODSRECORDTOP = "GOODSSRECORDTOP_";
    const std::string CREATE = "CREATE";

    void initialize() {
        xchain::Context* ctx = this->context();
        const std::string& admin = ctx->arg("admin");
        if (admin.empty()) {
            ctx->error("missing admin address");
            return;
        }

        ctx->put_object("admin", admin);
        ctx->ok("initialize success");
    }

    void addAdmin() {
        xchain::Context* ctx = this->context();
        const std::string& admin = ctx->arg("admin");
        if (admin.empty()) {
            ctx->error("missing admin address");
            return;
        }
        const std::string& caller = ctx->initiator();
        if (caller.empty()) {
            ctx->error("missing initiator");
            return;
        }
        if (!isAdmin(ctx, caller)) {
            ctx->error("only the admin can create new admin");
            return;
        }
        std::string oldAdmin;
        if (!ctx->get_object("admin", &oldAdmin)) {
            return;
        }
        const std::string& newAdmin=admin+";"+oldAdmin;

        ctx->put_object("admin", newAdmin);
        ctx->ok(newAdmin);
    }

    bool isAdmin(xchain::Context* ctx, const std::string& caller) {
        std::string admin;
        if (!ctx->get_object("admin", &admin)) {
            return false;
        }
        std::string::size_type idx = admin.find( caller );
        if( idx != std::string::npos ){
            return true;
        }else{
            return false;
        }
    }

    void createGoods() {
        xchain::Context* ctx = this->context();
        const std::string& caller = ctx->initiator();
        if (caller.empty()) {
            ctx->error("missing initiator");
            return;
        }

        if (!isAdmin(ctx, caller)) {
            ctx->error("only the admin can create new goods");
            return;
        }

        const std::string& id = ctx->arg("id");
        if (id.empty()) {
            ctx->error("missing 'id' as goods identity");
            return;
        }

        const std::string& desc = ctx->arg("desc");
        if (desc.empty()) {
            ctx->error("missing 'desc' as goods desc");
            return;
        }

        std::string goodsKey =  GOODS + id;
        std::string value;
        if (ctx->get_object(goodsKey, &value)) {
            ctx->error("the id is already exist, please check again");
            return;
        }
        ctx->put_object(goodsKey, desc);

        std::string goodsRecordsKey =  GOODSRECORD + id + "_0";
        std::string goodsRecordsTopKey =  GOODSRECORDTOP + id;
        ctx->put_object(goodsRecordsKey, CREATE);
        ctx->put_object(goodsRecordsTopKey, 0);
        ctx->ok(id);
    }

    void updateGoods() {
        xchain::Context* ctx = this->context();
        const std::string& caller = ctx->initiator();
        if (caller.empty()) {
            ctx->error("missing initiator");
            return;
        }

        if (!isAdmin(ctx, caller)) {
            ctx->error("only the admin can create new goods");
            return;
        }

        const std::string& id = ctx->arg("id");
        if (id.empty()) {
            ctx->error("missing 'id' as goods identity");
            return;
        }

        const std::string& reason = ctx->arg("reason");
        if (reason.empty()) {
            ctx->error("missing 'reason' as update reason");
            return;
        }

        std::string goodsRecordsTopKey = GOODSRECORDTOP + id;
        std::string value;
        ctx->get_object(goodsRecordsTopKey, &value);
        int topRecord = 0;
        topRecord = atoi(value.c_str()) + 1;

        char topRecordVal[32];
        snprintf(topRecordVal, 32, "%d", topRecord);
        std::string goodsRecordsKey =  GOODSRECORD + id + "_" + topRecordVal;

        ctx->put_object(goodsRecordsKey, reason);
        ctx->put_object(goodsRecordsTopKey, topRecordVal);
        ctx->ok(topRecordVal);
    }

    void queryRecords() {
        xchain::Context* ctx = this->context();
        const std::string& id = ctx->arg("id");
        if (id.empty()) {
            ctx->error("missing 'id' as goods identity");
            return;
        }

        std::string goodsKey =  GOODS + id;
        std::string value;
        if (!ctx->get_object(goodsKey, &value)) {
            ctx->error("the id not exist, please check again");
            return;
        }

        std::string goodsRecordsKey = GOODSRECORD + id;
        std::unique_ptr<xchain::Iterator> iter =
            ctx->new_iterator(goodsRecordsKey, goodsRecordsKey + "~");

        std::string result = "\n";
        while (iter->next()) {
            std::pair<std::string, std::string> res;
            iter->get(&res);
            if (res.first.length() > goodsRecordsKey.length()) {
                std::string goodsRecord = res.first.substr(GOODSRECORD.length());
                std::string::size_type pos = goodsRecord.find("_");
                std::string goodsId = goodsRecord.substr(0, pos);
                std::string updateRecord = goodsRecord.substr(pos+1, goodsRecord.length());
                std::string reason = res.second;

                result += "goodsId=" + goodsId + ",updateRecord=" + updateRecord +
                          ",reason=" + reason + '\n';
            }
        }
        ctx->ok(result);
    }  
};

DEFINE_METHOD(SourceTraceDemo, initialize) {
    self.initialize();
}

DEFINE_METHOD(SourceTraceDemo, addAdmin) {
    self.addAdmin();
}

DEFINE_METHOD(SourceTraceDemo, createGoods) {
    self.createGoods();
}

DEFINE_METHOD(SourceTraceDemo, updateGoods) {
    self.updateGoods();
}

DEFINE_METHOD(SourceTraceDemo, queryRecords) {
    self.queryRecords();
}

XuperChain框架文档

XuperChain开源项目

其中,initialize函数会在合约被创建时自动调用,在创建时传入对应参数即可,这里需要传入admin以及admin对应的账户地址

将智能合约部署到开放网络

注册百度超级链

进入开放网络

image-20220604204331069

注册后获得私钥与助记词

image-20220604204542856

进入控制台后创建合约账户(需要花2rmb*[我这里搞活动只需要1rmb]*)

image-20220604204247111

配置XuperIDE

将节点切换至Remote的Baidu Xuper

image-20220604205243258

导入密钥对

image-20220604205355169

image-20220604205441403

选中刚刚导入的密钥对

image-20220604205535227

如果这里已经创建了合约账户的话,可以看到此密钥对下已经有刚刚创建好的合约账户了

通过XuperIDE部署合约到开放网络

注意:使用XuperIDE的时候我使用 魔法上网-全局 才可以构建,否则会弹出time out错误,目前版本是3.10.0,大概率是因为github连接时间超过限制时间了,仅供参考

构建完成后部署到合约账户

image-20220604211709952

注意:开放网络好像指能部署C++或者Solidity编写的智能合约,经过测试Go编写的智能合约无法被部署,报错Error,无ErrorMessage

因此我这里后来改成了用C++编写上链,过程相同

使用GO sdk开发后端

尝试使用python sdk的失败

试了好久的python sdk,因为自己习惯用python写后端,结果发现

image-20220605214141952

麻了。。。

新建Go后端项目

这里可以使用sdk里的example改,下述代码为example中的contract.go

package main

import (
	"fmt"
	"io/ioutil"
	"time"

	"github.com/xuperchain/xuper-sdk-go/v2/account"
	"github.com/xuperchain/xuper-sdk-go/v2/xuper"
)

func main() {
	//testEVMContract()
	//testNativeContract()
	testWasmContract()

}

func testWasmContract() {
	codePath := "example/contract/data/counter.wasm"
	code, err := ioutil.ReadFile(codePath)
	if err != nil {
		panic(err)
	}

	xuperClient, err := xuper.New("127.0.0.1:37101")
	if err != nil {
		panic("new xuper Client error:")
	}

	// account, err := account.RetrieveAccount("玉 脸 驱 协 介 跨 尔 籍 杆 伏 愈 即", 1)
	acc, err := account.GetAccountFromPlainFile("example/contract/data/keys")
	if err != nil {
		fmt.Printf("retrieveAccount err: %v\n", err)
		return
	}
	fmt.Printf("retrieveAccount address: %v\n", acc.Address)
	contractAccount := "XC1111111111111111@xuper"
	timeStamp := fmt.Sprintf("%d", time.Now().Unix())
	contractName := fmt.Sprintf("counter%s", timeStamp[1:])
	fmt.Println(contractName)
	err = acc.SetContractAccount(contractAccount)

	args := map[string]string{
		"creator": "test",
		"key":     "test",
	}

	tx, err := xuperClient.DeployWasmContract(acc, contractName, code, args)
	if err != nil {
		panic(err)
	}
	fmt.Printf("Deploy wasm Success!TxID:%x\n", tx.Tx.Txid)

	tx, err = xuperClient.InvokeWasmContract(acc, contractName, "increase", args)
	if err != nil {
		panic(err)
	}
	fmt.Printf("Invoke Wasm Contract Success! TxID:%x\n", tx.Tx.Txid)

	tx, err = xuperClient.QueryWasmContract(acc, contractName, "get", args)
	if err != nil {
		panic(err)
	}
	fmt.Printf("Query Wasm Contract Success! Response:%s\n", tx.ContractResponse.Body)
}

func testEVMContract() {
	binPath := "./example/contract/Counter.bin"
	abiPath := "./example/contract/Counter.abi"

	bin, err := ioutil.ReadFile(binPath)
	if err != nil {
		panic(err)
	}
	abi, err := ioutil.ReadFile(abiPath)
	if err != nil {
		panic(err)
	}

	// 通过助记词恢复账户
	account, err := account.RetrieveAccount("玉 脸 驱 协 介 跨 尔 籍 杆 伏 愈 即", 1)
	if err != nil {
		fmt.Printf("retrieveAccount err: %v\n", err)
		return
	}
	fmt.Printf("retrieveAccount address: %v\n", account.Address)
	contractAccount := "XC1234567890123456@xuper"
	err = account.SetContractAccount(contractAccount)
	if err != nil {
		panic(err)
	}

	contractName := "SDKEvmContract"
	xchainClient, err := xuper.New("127.0.0.1:37101")

	args := map[string]string{
		"key": "test",
	}
	tx, err := xchainClient.DeployEVMContract(account, contractName, abi, bin, args)
	if err != nil {
		panic(err)
	}
	fmt.Printf("DeployEVMContract SUCCESS! %x\n", tx.Tx.Txid)

	tx, err = xchainClient.InvokeEVMContract(account, contractName, "increase", args)
	if err != nil {
		panic(err)
	}
	fmt.Printf("InvokeEVMContract SUCCESS! %x\n", tx.Tx.Txid)

	tx, err = xchainClient.QueryEVMContract(account, contractName, "get", args)
	if err != nil {
		panic(err)
	}
	fmt.Printf("InvokeEVMContract Success! Response:%s\n", tx.ContractResponse.Body)

}

func testNativeContract() {
	codePath := "./example/contract/counter" // 编译好的二进制文件
	code, err := ioutil.ReadFile(codePath)
	if err != nil {
		panic(err)
	}

	account, err := account.RetrieveAccount("玉 脸 驱 协 介 跨 尔 籍 杆 伏 愈 即", 1)
	if err != nil {
		fmt.Printf("retrieveAccount err: %v\n", err)
		return
	}
	fmt.Printf("retrieveAccount address: %v\n", account.Address)
	contractAccount := "XC1234567890123456@xuper"
	contractName := "SDKNativeCount1"
	err = account.SetContractAccount(contractAccount)
	if err != nil {
		panic(err)
	}

	xchainClient, err := xuper.New("127.0.0.1:37101")
	if err != nil {
		panic(err)
	}
	args := map[string]string{
		"creator": "test",
		"key":     "test",
	}
	tx, err := xchainClient.DeployNativeGoContract(account, contractName, code, args)
	if err != nil {
		panic(err)
	}
	fmt.Printf("Deploy Native Go Contract Success! %x\n", tx.Tx.Txid)

	tx, err = xchainClient.InvokeNativeContract(account, contractName, "increase", args)
	if err != nil {
		panic(err)
	}
	fmt.Printf("Invoke Native Go Contract Success! %x\n", tx.Tx.Txid)

	tx, err = xchainClient.QueryNativeContract(account, contractName, "get", args)
	if err != nil {
		panic(err)
	}
	if tx != nil {
		fmt.Printf("查询结果:%s\n", tx.ContractResponse.Body)
	}
}
go mod init
go mod tidy

创建go.mod文件并自动根据代码导入相应的包(相当于GO的包管理工具)

go build test.go

出现如下错误

image-20220606113119857

根据stack overflow上这篇帖子解决 image-20220606113519469

测试合约调用函数

func testWasmContract() {
	xuperClient, err := xuper.New("39.156.69.83:37100")
	if err != nil {
		panic("new xuper Client error:")
	}

	account, err := account.RetrieveAccount("x x x x x x x x x x x x", 1)
	if err != nil {
		fmt.Printf("retrieveAccount err: %v\n", err)
		return
	}
	fmt.Printf("retrieveAccount address: %v\n", account.Address)

	var contractName string="antifakecon"

	args := map[string]string{
		"id": "356a192b7913b04c54574d18c28d46e6395428ab",
	}

	// tx, err = xuperClient.InvokeWasmContract(acc, contractName, "increase", args)
	// if err != nil {
	// 	panic(err)
	// }
	// fmt.Printf("Invoke Wasm Contract Success! TxID:%x\n", tx.Tx.Txid)

	tx, err := xuperClient.QueryWasmContract(account, contractName, "queryRecords", args)
	if err != nil {
		fmt.Printf("query err: %v\n", err)
	}
	fmt.Printf("Query Wasm Contract Success! Response:%s\n", tx.ContractResponse.Body)
}

注意这里的助记词应为开放网络上已经注册了合约账户的私钥对应助记词,否则返回如下

image-20220606114520268

修改后可正常调用

image-20220606114610115

使用GO Gin框架搭建简单后端

简单的Hello World测试

Go官方文档Gin框架部分

package main

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

func main() {
    // 1.创建路由
   r := gin.Default()
   // 2.绑定路由规则,执行的函数
   // gin.Context,封装了request和response
   r.GET("/", func(c *gin.Context) {
      c.String(http.StatusOK, "hello World!")
   })
   // 3.监听端口,默认在8080
   // Run("里面不指定端口号默认为8080") 
   r.Run(":8000")
}

测试Hello World,更新导入后要重新执行go mod tidy

go mod tidy

这里报错,导致go.mod未被修改

image-20220606114946976

通过参考这篇帖子解决

image-20220606115128894

测试代码,执行

go mod tidy
go build test.go
./test

image-20220606115440735

image-20220606115528981

测试成功

编写后端Query函数

func main() {
	// testWasmContract()
	// 1.创建路由
	r := gin.Default()
	// 2.绑定路由规则,执行的函数
	// gin.Context,封装了request和response
	r.POST("/query", func(c *gin.Context) {
		fmt.Printf("id:")
        id := c.PostForm("id")
		fmt.Printf(id)
		resp:=queryRecords(id)
		c.JSON(200, gin.H{"resp": resp})
    })
	// 3.监听端口,默认在8080
	// Run("里面不指定端口号默认为8080") 
	r.Run(":8000")
}

func queryRecords(id string) string {
	xuperClient, err := xuper.New("39.156.69.83:37100")
	if err != nil {
		resp:= fmt.Sprintf("new xuper Client error: %v\n", err)
		return resp
	}
	account, err := account.RetrieveAccount("铝 纵 殿 熙 何 云 关 棒 悟 信 析 造", 1)
	if err != nil {
		resp:= fmt.Sprintf("retrieveAccount err: %v\n", err)
		return resp
	}
	var contractName string="antifakecon"
	args := map[string]string{
		"id": id,
	}
	fmt.Printf(id)
	tx, err := xuperClient.QueryWasmContract(account, contractName, "queryRecords", args)
	if err != nil {
		resp:=fmt.Sprintf("query err: %v\n", err)
		return resp
	}
	resp:=fmt.Sprintf("%s", tx.ContractResponse.Body)
	return resp
}

前端可以通过PostMan等游览器插件模拟发送请求进行测试,示例:

image-20220606161336413

我这里已经写了一半前端,就直接Axios进行请求了

测试发现报错

No 'Access-Control-Allow-Origin' header is present on the requested resource

在后端添加跨域配置

// 配置跨域函数
func Cors() gin.HandlerFunc {
	return func(c *gin.Context) {
		method := c.Request.Method
                origin := c.Request.Header.Get("Origin")
		if origin != "" {
			c.Header("Access-Control-Allow-Origin", "*")
			c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
			c.Header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization")
			c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Cache-Control, Content-Language, Content-Type")
			c.Header("Access-Control-Allow-Credentials", "true")
		}
		if method == "OPTIONS" {
			c.AbortWithStatus(http.StatusNoContent)
		}
		c.Next()
	}
}

func main() {
	// testWasmContract()
	// 1.创建路由
	r := gin.Default()
	// 开启跨域
	r.Use(Cors())
	// 2.绑定路由规则,执行的函数
	// gin.Context,封装了request和response
	r.POST("/query", func(c *gin.Context) {
		fmt.Printf("id:")
        id := c.PostForm("id")
		fmt.Printf(id)
		resp:=queryRecords(id)
		c.JSON(200, gin.H{"resp": resp})
    })
	// 3.监听端口,默认在8080
	// Run("里面不指定端口号默认为8080") 
	r.Run(":8000")
}

运行发现后端获取的id为空,根据这篇帖子修复

import axios from 'axios';
import qs from 'qs';

const rootUrl = 'http://localhost:8000';

const instance = axios.create({
    baseURL: rootUrl,
    timeout: 3000,
    headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
    },
});

export function queryGood(id) {
    return instance({
        method: 'post',
        url: '/query',
        data: qs.stringify({
            id: id,
        }),
    });
}

成功获取到Query返回值

image-20220606161830659

编写后端其他函数

至此我们的后端大体框架已经搭建好了,剩下的工作就很简单了

注意:连接开放网络需要配置conf/sdk.yaml,如下图

image-20220606175435786

否则会有如下报错

image-20220606175259950

sdk.yaml内容如下

# endorseService Info
# testNet addrs
endorseServiceHost: "39.156.69.83:37100"
complianceCheck:
  # 是否需要进行合规性背书
  isNeedComplianceCheck: true
  # 是否需要支付合规性背书费用
  isNeedComplianceCheckFee: true
  # 合规性背书费用
  complianceCheckEndorseServiceFee: 400
  # 支付合规性背书费用的收款地址
  complianceCheckEndorseServiceFeeAddr: aB2hpHnTBDxko3UoP2BpBZRujwhdcAFoT
  # 如果通过合规性检查,签发认证签名的地址
  complianceCheckEndorseServiceAddr: jknGxa6eyum1JrATWvSJKW3thJ9GKHA9n
#创建平行链所需要的最低费用
minNewChainAmount: "100"
crypto: "xchain"
txVersion: 1

注意缩进!注意缩进!注意缩进!卡了我好久

若连接开放网络,那两个true需要保持,否则可以默认false

image-20220606175945903

看我有多少报错就知道我卡了多久了QAQ

接下来三个函数大同小异,可供参考

create函数
r.POST("/create", func(c *gin.Context) {
    id := c.PostForm("id")
		desc := c.PostForm("desc")
		account := c.PostForm("account")
		contractAccount := c.PostForm("contractAccount")
		resp:=addRecords(id,desc,account,contractAccount)
		c.JSON(200, gin.H{"resp": resp})
})

func addRecords(id string,desc string,key string,contractAccount string) string {
	xuperClient, err := xuper.New("39.156.69.83:37100")
	if err != nil {
		resp:= fmt.Sprintf("new xuper Client error: %v\n", err)
		return resp
	}
	account, err := account.RetrieveAccount(key, 1)
	if err != nil {
		resp:= fmt.Sprintf("retrieveAccount err: %v\n", err)
		return resp
	}
	err = account.SetContractAccount(contractAccount)
	if err != nil {
		resp:= fmt.Sprintf("setContractAccount err: %v\n", err)
		return resp
	}
	var contractName string="antifakecon"
	args := map[string]string{
		"id": id,
		"desc":desc,
	}
	tx, err := xuperClient.InvokeWasmContract(account, contractName, "createGoods", args)
	if err != nil {
		resp:=fmt.Sprintf("create err: %v\n", err)
		return resp
	}
	resp:=fmt.Sprintf("data:%s TxID:%x", tx.ContractResponse.Body, tx.Tx.Txid)
	return resp
}
update函数
r.POST("/update", func(c *gin.Context) {
    id := c.PostForm("id")
		desc := c.PostForm("desc")
		account := c.PostForm("account")
		contractAccount := c.PostForm("contractAccount")
		resp:=updateRecords(id,desc,account,contractAccount)
		c.JSON(200, gin.H{"resp": resp})
})

func updateRecords(id string,desc string,key string,contractAccount string) string {
	xuperClient, err := xuper.New("39.156.69.83:37100")
	if err != nil {
		resp:= fmt.Sprintf("new xuper Client error: %v\n", err)
		return resp
	}
	account, err := account.RetrieveAccount(key, 1)
	if err != nil {
		resp:= fmt.Sprintf("retrieveAccount err: %v\n", err)
		return resp
	}
	err = account.SetContractAccount(contractAccount)
	if err != nil {
		resp:= fmt.Sprintf("setContractAccount err: %v\n", err)
		return resp
	}
	var contractName string="antifakecon"
	args := map[string]string{
		"id": id,
		"reason":desc,
	}
	tx, err := xuperClient.InvokeWasmContract(account, contractName, "updateGoods", args)
	if err != nil {
		resp:=fmt.Sprintf("update err: %v\n", err)
		return resp
	}
	resp:=fmt.Sprintf("data:%s TxID:%x", tx.ContractResponse.Body, tx.Tx.Txid)
	return resp
}
addAdmin函数
r.POST("/addAdmin", func(c *gin.Context) {
    address := c.PostForm("address")
		account := c.PostForm("account")
		contractAccount := c.PostForm("contractAccount")
		resp:=addAdmin(address,account,contractAccount)
		c.JSON(200, gin.H{"resp": resp})
})

func addAdmin(address string,key string,contractAccount string) string {
	xuperClient, err := xuper.New("39.156.69.83:37100")
	if err != nil {
		resp:= fmt.Sprintf("new xuper Client error: %v\n", err)
		return resp
	}
	account, err := account.RetrieveAccount(key, 1)
	if err != nil {
		resp:= fmt.Sprintf("retrieveAccount err: %v\n", err)
		return resp
	}
	err = account.SetContractAccount(contractAccount)
	if err != nil {
		resp:= fmt.Sprintf("setContractAccount err: %v\n", err)
		return resp
	}
	var contractName string="antifakecon"
	args := map[string]string{
		"admin": address,
	}
	tx, err := xuperClient.InvokeWasmContract(account, contractName, "addAdmin", args)
	if err != nil {
		resp:=fmt.Sprintf("update err: %v\n", err)
		return resp
	}
	resp:=fmt.Sprintf("data:%s TxID:%x", tx.ContractResponse.Body, tx.Tx.Txid)
	return resp
}

使用React开发前端

初始化React前端项目

个人常用React写网页,所以这里就也用React了。

React文档

命令行输入下列命令初始化react项目

❯ yarn create react-app anti-fake

image-20220605163707621

没有安装yarn的,可以自行百度安装/或者使用npm初始化

❯ npx create-react-app anti-fake

启动项目

❯ cd anti-fake
❯ yarn start

image-20220605164152007

导入ant-design

ant-design文档

根据官方文档导入ant-design

❯ yarn add antd

修改 src/App.js,引入 antd 的按钮组件。

import React from 'react';
import { Button } from 'antd';
import './App.css';

const App = () => (
  <div className="App">
    <Button type="primary">Button</Button>
  </div>
);

export default App;

修改 src/App.css,在文件顶部引入 antd/dist/antd.css

@import '~antd/dist/antd.css';

image-20220605164634017

看到一个按钮,说明ant-design组件导入成功

页面开发

ant-design组件文档

有关Tabs标签页下的线条动画问题解决方案-Github issues-35631

Tabs

image-20220605173030937

image-20220605173054811

查询表单

image-20220605175125142

image-20220605175153951

使用JS sdk调用智能合约(放弃,决定使用前后端方式来调用)

npm install @xuperchain/xuper-sdk -save

image-20220605175541070

❯ npm install –save @xuperchain/xuper-sdk
npm ERR! arg Argument starts with non-ascii dash, this is probably invalid: –save
npm ERR! code EINVALIDTAGNAME
npm ERR! Invalid tag name "–save": Tags may not have any characters that encodeURIComponent encodes.

npm ERR! A complete log of this run can be found in:
npm ERR!     /Users/bytedance/.npm/_logs/2022-06-05T09_53_56_678Z-debug-0.log

注意:官方命令中的-save可能会报上述错误,移动至命令最后即可

❯ npm install @xuperchain/xuper-sdk -save

added 50 packages, and audited 1371 packages in 8s

194 packages are looking for funding
  run `npm fund` for details

8 high severity vulnerabilities

To address all issues (including breaking changes), run:
  npm audit fix --force

Run `npm audit` for details.
放弃用JS sdk了,有毒。。。

Axios进行网络请求

import axios from 'axios';
import qs from 'qs';

const rootUrl = 'http://localhost:8000';

const instance = axios.create({
    baseURL: rootUrl,
    timeout: 3000,
    headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
    },
});

export function queryGood(id) {
    return instance({
        method: 'post',
        url: '/query',
        data: qs.stringify({
            id: id,
        }),
    });
}

export function createGood(id, desc, account, contractAccount) {
    return instance({
        method: 'post',
        url: '/create',
        data: qs.stringify({
            id: id,
            desc: desc,
            account: account,
            contractAccount: contractAccount,
        }),
    });
}

export function updateGood(id, desc, account, contractAccount) {
    return instance({
        method: 'post',
        url: '/update',
        data: qs.stringify({
            id: id,
            desc: desc,
            account: account,
            contractAccount: contractAccount,
        }),
    });
}

export function addAdmin(address, account, contractAccount) {
    return instance({
        method: 'post',
        url: '/addAdmin',
        data: qs.stringify({
            address: address,
            account: account,
            contractAccount: contractAccount,
        }),
    });
}

注意应使用qs.stringify并设置header,否则后端无法读取,前文已有提及

create以及update表单

import { Form, Input, Button, Select, notification, message } from 'antd';
import { useState } from 'react';
import { createGood, updateGood } from './Utils/Axios';

const { Option } = Select;

function CreateGoods() {
    const [opt, setOpt] = useState('create');

    const openNotification = (title, des) => {
        const args = {
            message: title,
            description: des,
            duration: 0,
        };
        notification.open(args);
    };

    const showMessage = (resp, mess) => {
        if (resp.indexOf('TxID' !== -1)) {
            message.success(mess + '成功');
            const temp = resp.search('TxID');
            openNotification(mess + '成功', resp.slice(temp));
        } else {
            message.error('发生错误');
            openNotification('发生错误', resp);
        }
    };

    const onFinish = (values) => {
        let resp;
        if (opt === 'create') {
            createGood(values.id, values.desc, values.account, values.contractAccount).then((res) => {
                resp = res.data.resp;
                showMessage(resp, '添加');
            });
        } else {
            updateGood(values.id, values.desc, values.account, values.contractAccount).then((res) => {
                resp = res.data.resp;
                showMessage(resp, '更新');
            });
        }
    };

    const onOptChange = (value) => {
        setOpt(value);
    };

    const onFinishFailed = (errorInfo) => {
        console.log('Failed:', errorInfo);
    };

    return (
        <div style={{ marginTop: '3vh', marginBottom: '6vh' }}>
            <Form name="basic" labelCol={{ offset: 2, span: 3 }} wrapperCol={{ offset: 1, span: 15 }} initialValues={{ remember: true }} onFinish={onFinish} onFinishFailed={onFinishFailed} autoComplete="off" size={'large'}>
                <Form.Item name="opt" label="opt" rules={[{ required: true }]}>
                    <Select placeholder="请选择你的操作" onChange={onOptChange} allowClear>
                        <Option value="create">创建</Option>
                        <Option value="update">更新</Option>
                    </Select>
                </Form.Item>
                <Form.Item label="id" name="id" rules={[{ required: true, message: '请输入ID' }]}>
                    <Input />
                </Form.Item>
                <Form.Item label="desc" name="desc" rules={[{ required: true, message: '请输入描述' }]}>
                    <Input />
                </Form.Item>
                <Form.Item label="account" name="account" rules={[{ required: true, message: '请输入账号助记词' }]}>
                    <Input />
                </Form.Item>
                <Form.Item label="contractAccount" name="contractAccount" rules={[{ required: true, message: '请输入合约账号' }]}>
                    <Input />
                </Form.Item>

                <Form.Item wrapperCol={{ offset: 2, span: 24 }}>
                    <Button type="primary" htmlType="submit" style={{ width: '40vw', margin: 'auto auto' }}>
                        {opt === 'create' ? '创建' : '更新'}
                    </Button>
                </Form.Item>
            </Form>
        </div>
    );
}

export default CreateGoods;

addAdmin表单

import { Form, Input, Button, notification, message } from 'antd';
import { addAdmin } from './Utils/Axios';

function AddCompany() {
    const openNotification = (title, des) => {
        const args = {
            message: title,
            description: des,
            duration: 0,
        };
        notification.open(args);
    };

    const onFinish = (values) => {
        addAdmin(values.address, values.account, values.contractAccount).then((res) => {
            const resp = res.data.resp;
            if (resp.indexOf('TxID') !== -1) {
                message.success('添加成功');
                const temp = resp.search('TxID');
                openNotification('添加成功', resp.slice(temp));
            } else {
                message.error('发生错误');
                openNotification('发生错误', resp);
            }
        });
    };

    const onFinishFailed = (errorInfo) => {
        console.log('Failed:', errorInfo);
    };

    return (
        <div style={{ marginTop: '3vh', marginBottom: '6vh' }}>
            <Form name="basic" labelCol={{ offset: 2, span: 3 }} wrapperCol={{ offset: 1, span: 15 }} initialValues={{ remember: true }} onFinish={onFinish} onFinishFailed={onFinishFailed} autoComplete="off" size={'large'}>
                <Form.Item label="address" name="address" rules={[{ required: true, message: '请输入地址' }]}>
                    <Input />
                </Form.Item>
                <Form.Item label="account" name="account" rules={[{ required: true, message: '请输入账号助记词' }]}>
                    <Input />
                </Form.Item>
                <Form.Item label="contractAccount" name="contractAccount" rules={[{ required: true, message: '请输入合约账号' }]}>
                    <Input />
                </Form.Item>

                <Form.Item wrapperCol={{ offset: 2, span: 24 }}>
                    <Button type="primary" htmlType="submit" style={{ width: '40vw', margin: 'auto auto' }}>
                        添加
                    </Button>
                </Form.Item>
            </Form>
        </div>
    );
}

export default AddCompany;

为query表单的结果添加了时间轴组件

import { Form, Input, Button, Timeline } from 'antd';
import { useState } from 'react';
import { queryGood } from './Utils/Axios';

function QueryGoods() {
    const [opt, setOpt] = useState([]);

    const onFinish = (values) => {
        queryGood(values.id).then((res) => {
            const resp = res.data.resp;
            const tempSplit = resp.split('\n');
            let temp = [];
            tempSplit.forEach((ele) => {
                if (ele.search('reason') !== -1) {
                    const addTemp = ele.slice(ele.search('reason') + 7);
                    temp.push(addTemp);
                }
            });
            setOpt(temp);
        });
    };

    const onFinishFailed = (errorInfo) => {
        console.log('Failed:', errorInfo);
    };

    const timeLine = opt.map((ele, index) => <Timeline.Item key={index}>{ele}</Timeline.Item>);

    return (
        <div style={{ marginTop: '3vh', marginBottom: '6vh' }}>
            <Form name="basic" labelCol={{ offset: 2 }} wrapperCol={{ offset: 2, span: 16 }} initialValues={{ remember: true }} onFinish={onFinish} onFinishFailed={onFinishFailed} autoComplete="off" size={'large'}>
                <Form.Item label="id" name="id" rules={[{ required: true, message: 'Please input id!' }]}>
                    <Input />
                </Form.Item>

                <Form.Item wrapperCol={{ offset: 2, span: 24 }}>
                    <Button type="primary" htmlType="submit" style={{ width: '40vw', margin: 'auto auto' }}>
                        查询
                    </Button>
                </Form.Item>
            </Form>
            <Timeline mode="alternate">{timeLine}</Timeline>
        </div>
    );
}

export default QueryGoods;

前后端最终效果

添加商品

image-20220606200802467

image-20220606200949328

image-20220606201047238

更新商品

image-20220606201436190

image-20220606201509030

image-20220606201528960

查询商品

image-20220606201723423

image-20220606201800211

新增合作公司

image-20220606201953373

image-20220606202113836

测试新增公司是否持有权限

使用新增公司账号进行更新操作

image-20220606202359014

前后端上线

具体上线流程就不多说了,展示地址