基于 XuperChain 框架的区块链商品防伪系统
展示地址:https://anti.shellmiao.com/
Git地址:https://git.shellmiao.com/Shellmiao/AntiFake/src/master
使用需要使用Admin助记词和合约账户作为身份认证
这里可用测试账号:
- 助记词:铝 纵 殿 熙 何 云 关 棒 悟 信 析 造
- 合约账号:XC0751444561687239@xuper
序言
在学习的过程中发现百度超级链 XuperChain 的相关资料比较少,有些也不太准确,所以记录下来自己的学习过程,希望能帮助到更多的人,欢迎关注我的博客
什么是区块链
区块链技术的核心概念是分布式账本,区块链网络中的每个节点对 账本/数据/状态 都存有相同的副本,因此想要篡改 账本/数据/状态 的成本是极其高的。改变记录的唯一方法是让大多数节点同意这样做。
什么是智能合约
以太坊(区块链2.0)和比特币(区块链1.0)的不同是以太坊具有可编程特性,添加了对智能合约的支持。而智能合约则是可以在区块链上运行的代码
智能合约是区块链网络上的一种特殊账户
- 用户账户
- 地址
- 余额
- 智能合约账户
- 地址
- 余额
- 代码
- 状态
智能合约中的状态是智能合约中声明的所有变量和变量的当前状态,这些变量会一直存在于区块链网络中并依赖区块链网络保证其不可篡改性
智能合约中的代码是编译后可以在区块链网络上运行的字节码
创建和调用智能合约作为交易的一种形式被区块链记录
以太坊和比特币在交易层面的区别
- 价值传递 (和比特币相同)
- TO :收款地址
- DATA :留空或留言信息
- FROM :谁发出
- AMOUNT :发送多少
- 创建合约
- TO :留空 (触发创建智能合约)
- DATA :包含编译为字节码的智能合约代码
- FROM :谁创建
- AMOUNT :可以是零或任何数量的以太,它是我们想要给合约的存款
- 调用合约函数
- TO: 目标合约账户地址
- DATA: 包含函数名称和参数 – 标识如何调用智能合约函数
- FROM :谁调用
- AMOUNT : 可以是零或任意数量的以太,例如可以支付合约服务费用
智能合约的工作流程
构建合约
多方用户共同参与制定一份智能合约,其中明确规定了交易双方的权利和义务,开发人员将这些权利和义务以电子化的方式进行编程,代码中包含会触发合约自动执行的条件
部署合约
合约通过P2P网络扩散并存入区块链,全网的各个节点都可以接收到这份合约
调用合约
智能合约会定期检查是否存在相关事件和触发条件,将满足条件的事件推送到待验证的队列中,区块链上的验证节点先对事件进行签名认证,以确保其有效性,等大多数验证节点对该事件达成共识,智能合约将成功执行,并通知用户
注意:合约不能主动执行,它只能被外部账户发起调用。如果一个合约要定期执行,那只能由线下服务器定期发起合约调用
设计思路
溯源,就是对商品生产、销售的各个环节进行记录,在传统记录条件下,记录容易被篡改,例如生产商内部人员恶意在数据库中新增不能存在的商品数据,并卖给盗版厂商,导致盗版商品难以被鉴别。
但是在区块链环境下,恶意篡改区块链字段的成本极为高昂,可以认为几乎是不可能的。因此我们需要将商品的溯源信息部署在区块链上。这里有两种思路:
- 直接将溯源信息作为智能合约的一个变量上链,但是对溯源信息的要求是不能过于复杂
- 将溯源信息进行存储,对溯源信息的每一次改变执行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
开发商品溯源智能合约
官方有各种合约模版可供参考和魔改
// 在我使用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编写合约
这里使用了商品溯源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();
}
其中,initialize函数会在合约被创建时自动调用,在创建时传入对应参数即可,这里需要传入admin以及admin对应的账户地址
将智能合约部署到开放网络
注册百度超级链
进入开放网络
注册后获得私钥与助记词
进入控制台后创建合约账户(需要花2rmb[我这里搞活动只需要1rmb])
配置XuperIDE
将节点切换至Remote的Baidu Xuper
导入密钥对
选中刚刚导入的密钥对
如果这里已经创建了合约账户的话,可以看到此密钥对下已经有刚刚创建好的合约账户了
通过XuperIDE部署合约到开放网络
注意:使用XuperIDE的时候我使用 魔法上网-全局 才可以构建,否则会弹出time out错误,目前版本是3.10.0,大概率是因为github连接时间超过限制时间了,仅供参考
构建完成后部署到合约账户
注意:开放网络好像指能部署C++或者Solidity编写的智能合约,经过测试Go编写的智能合约无法被部署,报错Error,无ErrorMessage
因此我这里后来改成了用C++编写上链,过程相同
使用GO sdk开发后端
尝试使用python sdk的失败
试了好久的python sdk,因为自己习惯用python写后端,结果发现
麻了。。。
新建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
出现如下错误
测试合约调用函数
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)
}
注意这里的助记词应为开放网络上已经注册了合约账户的私钥对应助记词,否则返回如下
修改后可正常调用
使用GO Gin框架搭建简单后端
简单的Hello World测试
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未被修改
通过参考这篇帖子解决
测试代码,执行
go mod tidy
go build test.go
./test
测试成功
编写后端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等游览器插件模拟发送请求进行测试,示例:
我这里已经写了一半前端,就直接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返回值
编写后端其他函数
至此我们的后端大体框架已经搭建好了,剩下的工作就很简单了
注意:连接开放网络需要配置conf/sdk.yaml,如下图
否则会有如下报错
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
看我有多少报错就知道我卡了多久了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项目
❯ yarn create react-app anti-fake
没有安装yarn的,可以自行百度安装/或者使用npm初始化
❯ npx create-react-app anti-fake
启动项目
❯ cd anti-fake
❯ yarn start
导入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';
看到一个按钮,说明ant-design组件导入成功
页面开发
有关Tabs标签页下的线条动画问题解决方案-Github issues-35631
Tabs
查询表单
使用JS sdk调用智能合约(放弃,决定使用前后端方式来调用)
npm install @xuperchain/xuper-sdk -save
❯ 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;
前后端最终效果
添加商品
更新商品
查询商品
新增合作公司
测试新增公司是否持有权限
使用新增公司账号进行更新操作
前后端上线
具体上线流程就不多说了,展示地址
基于 XuperChain 框架的区块链商品防伪系统
展示地址:https://anti.shellmiao.com/
Git地址:https://git.shellmiao.com/Shellmiao/AntiFake/src/master
使用需要使用Admin助记词和合约账户作为身份认证
这里可用测试账号:
- 助记词:铝 纵 殿 熙 何 云 关 棒 悟 信 析 造
- 合约账号:XC0751444561687239@xuper
序言
在学习的过程中发现百度超级链 XuperChain 的相关资料比较少,有些也不太准确,所以记录下来自己的学习过程,希望能帮助到更多的人,欢迎关注我的博客
什么是区块链
区块链技术的核心概念是分布式账本,区块链网络中的每个节点对 账本/数据/状态 都存有相同的副本,因此想要篡改 账本/数据/状态 的成本是极其高的。改变记录的唯一方法是让大多数节点同意这样做。
什么是智能合约
以太坊(区块链2.0)和比特币(区块链1.0)的不同是以太坊具有可编程特性,添加了对智能合约的支持。而智能合约则是可以在区块链上运行的代码
智能合约是区块链网络上的一种特殊账户
- 用户账户
- 地址
- 余额
- 智能合约账户
- 地址
- 余额
- 代码
- 状态
智能合约中的状态是智能合约中声明的所有变量和变量的当前状态,这些变量会一直存在于区块链网络中并依赖区块链网络保证其不可篡改性
智能合约中的代码是编译后可以在区块链网络上运行的字节码
创建和调用智能合约作为交易的一种形式被区块链记录
以太坊和比特币在交易层面的区别
- 价值传递 (和比特币相同)
- TO :收款地址
- DATA :留空或留言信息
- FROM :谁发出
- AMOUNT :发送多少
- 创建合约
- TO :留空 (触发创建智能合约)
- DATA :包含编译为字节码的智能合约代码
- FROM :谁创建
- AMOUNT :可以是零或任何数量的以太,它是我们想要给合约的存款
- 调用合约函数
- TO: 目标合约账户地址
- DATA: 包含函数名称和参数 – 标识如何调用智能合约函数
- FROM :谁调用
- AMOUNT : 可以是零或任意数量的以太,例如可以支付合约服务费用
智能合约的工作流程
构建合约
多方用户共同参与制定一份智能合约,其中明确规定了交易双方的权利和义务,开发人员将这些权利和义务以电子化的方式进行编程,代码中包含会触发合约自动执行的条件
部署合约
合约通过P2P网络扩散并存入区块链,全网的各个节点都可以接收到这份合约
调用合约
智能合约会定期检查是否存在相关事件和触发条件,将满足条件的事件推送到待验证的队列中,区块链上的验证节点先对事件进行签名认证,以确保其有效性,等大多数验证节点对该事件达成共识,智能合约将成功执行,并通知用户
注意:合约不能主动执行,它只能被外部账户发起调用。如果一个合约要定期执行,那只能由线下服务器定期发起合约调用
设计思路
溯源,就是对商品生产、销售的各个环节进行记录,在传统记录条件下,记录容易被篡改,例如生产商内部人员恶意在数据库中新增不能存在的商品数据,并卖给盗版厂商,导致盗版商品难以被鉴别。
但是在区块链环境下,恶意篡改区块链字段的成本极为高昂,可以认为几乎是不可能的。因此我们需要将商品的溯源信息部署在区块链上。这里有两种思路:
- 直接将溯源信息作为智能合约的一个变量上链,但是对溯源信息的要求是不能过于复杂
- 将溯源信息进行存储,对溯源信息的每一次改变执行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
开发商品溯源智能合约
官方有各种合约模版可供参考和魔改
// 在我使用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编写合约
这里使用了商品溯源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();
}
其中,initialize函数会在合约被创建时自动调用,在创建时传入对应参数即可,这里需要传入admin以及admin对应的账户地址
将智能合约部署到开放网络
注册百度超级链
进入开放网络
注册后获得私钥与助记词
进入控制台后创建合约账户(需要花2rmb[我这里搞活动只需要1rmb])
配置XuperIDE
将节点切换至Remote的Baidu Xuper
导入密钥对
选中刚刚导入的密钥对
如果这里已经创建了合约账户的话,可以看到此密钥对下已经有刚刚创建好的合约账户了
通过XuperIDE部署合约到开放网络
注意:使用XuperIDE的时候我使用 魔法上网-全局 才可以构建,否则会弹出time out错误,目前版本是3.10.0,大概率是因为github连接时间超过限制时间了,仅供参考
构建完成后部署到合约账户
注意:开放网络好像指能部署C++或者Solidity编写的智能合约,经过测试Go编写的智能合约无法被部署,报错Error,无ErrorMessage
因此我这里后来改成了用C++编写上链,过程相同
使用GO sdk开发后端
尝试使用python sdk的失败
试了好久的python sdk,因为自己习惯用python写后端,结果发现
麻了。。。
新建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
出现如下错误
测试合约调用函数
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)
}
注意这里的助记词应为开放网络上已经注册了合约账户的私钥对应助记词,否则返回如下
修改后可正常调用
使用GO Gin框架搭建简单后端
简单的Hello World测试
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未被修改
通过参考这篇帖子解决
测试代码,执行
go mod tidy
go build test.go
./test
测试成功
编写后端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等游览器插件模拟发送请求进行测试,示例:
我这里已经写了一半前端,就直接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返回值
编写后端其他函数
至此我们的后端大体框架已经搭建好了,剩下的工作就很简单了
注意:连接开放网络需要配置conf/sdk.yaml,如下图
否则会有如下报错
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
看我有多少报错就知道我卡了多久了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项目
❯ yarn create react-app anti-fake
没有安装yarn的,可以自行百度安装/或者使用npm初始化
❯ npx create-react-app anti-fake
启动项目
❯ cd anti-fake
❯ yarn start
导入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';
看到一个按钮,说明ant-design组件导入成功
页面开发
有关Tabs标签页下的线条动画问题解决方案-Github issues-35631
Tabs
查询表单
使用JS sdk调用智能合约(放弃,决定使用前后端方式来调用)
npm install @xuperchain/xuper-sdk -save
❯ 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;
前后端最终效果
添加商品
更新商品
查询商品
新增合作公司
测试新增公司是否持有权限
使用新增公司账号进行更新操作
前后端上线
具体上线流程就不多说了,展示地址
原创文章,作者:Carrie001128,如若转载,请注明出处:https://blog.ytso.com/269335.html