区块链供应链金融实战2
  oCSbu1MUxU1k 2023年11月02日 44 0


在本篇博文中,我们将讲解在金链盟下,编写一个最简单的用户间转账的智能合约,并通过Java应用程序调用这个智能合约,为我们实现复杂的区块链供应链金融应用打下基础。
我们在这里利用Fisco Bcos提供的CRUD接口,来开发智能合约,供Web服务器来调用,从而实现各种复杂的业务逻辑。之所以选择CRUD接口,是因为该接口屏蔽了区块链的细节,使我们像做普通数据库应用开发一样来做智能合约开发。
我们首先创建资产管理表(t_cbdc):

字段名

中文名称

类型

备注

account

账户名称

string

主键

cbdc_value

余额

uint256

以分乘10000为单位

在FISCO BCOS中,智能合约通常使用solidity语言来编写,下面我们来实现上述业务逻辑。
我们要实现如下接口:

// 查询资产金额
function select(string account) public constant returns(int256, uint256)
// 资产注册
function register(string account, uint256 amount) public returns(int256)
// 资产转移
function transfer(string from_asset_account, string to_asset_account, uint256 amount) public returns(int256)

接下来我们实现cbdc.sol智能合约,这个智能合约与官网教程中的几乎一模一样,但是修改了名字:

pragma solidity ^0.4.24;

import "./Table.sol";

contract Cbdc {
// event
event RegisterEvent(int256 ret, string account, uint256 cbdc_value);
event TransferEvent(int256 ret, string from_account, string to_account, uint256 amount);

constructor() public {
// 构造函数中创建t_cbdc表
createTable();
}

function createTable() private {
TableFactory tf = TableFactory(0x1001);
// 资产管理表, key : account, field : cbdc_value
// | 资产账户(主键) | 资产金额 |
// |-------------------- |-------------------|
// | account | cbdc_value |
// |---------------------|-------------------|
//
// 创建表
tf.createTable("t_cbdc", "account", "cbdc_value");
}

function openTable() private returns(Table) {
TableFactory tf = TableFactory(0x1001);
Table table = tf.openTable("t_cbdc");
return table;
}

/*
描述 : 根据资产账户查询资产金额
参数 :
account : 资产账户

返回值:
参数一: 成功返回0, 账户不存在返回-1
参数二: 第一个参数为0时有效,资产金额
*/
function select(string account) public constant returns(int256, uint256) {
// 打开表
Table table = openTable();
// 查询
Entries entries = table.select(account, table.newCondition());
uint256 cbdc_value = 0;
if (0 == uint256(entries.size())) {
return (-1, cbdc_value);
} else {
Entry entry = entries.get(0);
return (0, uint256(entry.getInt("cbdc_value")));
}
}

/*
描述 : 资产注册
参数 :
account : 资产账户
amount : 资产金额
返回值:
0 资产注册成功
-1 资产账户已存在
-2 其他错误
*/
function register(string account, uint256 cbdc_value) public returns(int256){
int256 ret_code = 0;
int256 ret= 0;
uint256 temp_cbdc_value = 0;
// 查询账户是否存在
(ret, temp_cbdc_value) = select(account);
if(ret != 0) {
Table table = openTable();

Entry entry = table.newEntry();
entry.set("account", account);
entry.set("cbdc_value", int256(cbdc_value));
// 插入
int count = table.insert(account, entry);
if (count == 1) {
// 成功
ret_code = 0;
} else {
// 失败? 无权限或者其他错误
ret_code = -2;
}
} else {
// 账户已存在
ret_code = -1;
}

emit RegisterEvent(ret_code, account, cbdc_value);

return ret_code;
}

/*
描述 : 资产转移
参数 :
from_account : 转移资产账户
to_account : 接收资产账户
amount : 转移金额
返回值:
0 资产转移成功
-1 转移资产账户不存在
-2 接收资产账户不存在
-3 金额不足
-4 金额溢出
-5 其他错误
*/
function transfer(string from_account, string to_account, uint256 amount) public returns(int256) {
// 查询转移资产账户信息
int ret_code = 0;
int256 ret = 0;
uint256 from_cbdc_value = 0;
uint256 to_cbdc_value = 0;

// 转移账户是否存在?
(ret, from_cbdc_value) = select(from_account);
if(ret != 0) {
ret_code = -1;
// 转移账户不存在
emit TransferEvent(ret_code, from_account, to_account, amount);
return ret_code;

}

// 接受账户是否存在?
(ret, to_cbdc_value) = select(to_account);
if(ret != 0) {
ret_code = -2;
// 接收资产的账户不存在
emit TransferEvent(ret_code, from_account, to_account, amount);
return ret_code;
}

if(from_cbdc_value < amount) {
ret_code = -3;
// 转移资产的账户金额不足
emit TransferEvent(ret_code, from_account, to_account, amount);
return ret_code;
}

if (to_cbdc_value + amount < to_cbdc_value) {
ret_code = -4;
// 接收账户金额溢出
emit TransferEvent(ret_code, from_account, to_account, amount);
return ret_code;
}

Table table = openTable();

Entry entry0 = table.newEntry();
entry0.set("account", from_account);
entry0.set("cbdc_value", int256(from_cbdc_value - amount));
// 更新转账账户
int count = table.update(from_account, entry0, table.newCondition());
if(count != 1) {
ret_code = -5;
// 失败? 无权限或者其他错误?
emit TransferEvent(ret_code, from_account, to_account, amount);
return ret_code;
}

Entry entry1 = table.newEntry();
entry1.set("account", to_account);
entry1.set("cbdc_value", int256(to_cbdc_value + amount));
// 更新接收账户
table.update(to_account, entry1, table.newCondition());

emit TransferEvent(ret_code, from_account, to_account, amount);

return ret_code;
}
}

将Cbdc.sol放到fbc/console/contracts/solidity目录下,在fbc/console目录下,运行如下命令进行编译:

./sol2java com.arxandt.scf.fbc.sol.contract

如果编写的文件没有语法错误的话,编译好的文件将放在console/contracts/sdk目录下。
下载官网中的java工程​​​asset-app.tar.gz​​​,我们设scf/fbc为根目录,在该目录下解压asset-app.tar.gz。
将生成的java文件拷贝到对应的目录下:

cp console/contracts/sdk/java/com/arxandt/scf/fbc/sol/contract/Cbdc.java asset-app/src/main/java/com/arxandt/scf/fbc/sol/contract/.

将合约源码拷贝到/src/main/resources/contract目录下

cp console/contracts/solidity/*.sol asset-app/src/main/resources/contract/.

进入asset-app目录下,拷贝证书文件:

cp ../nodes/0.0.0.0/sdk/* src/main/resources/.

这是一个gradle的java工程,我们首先编译该工程,看看有没有问题:

./gradlew build

编译之后,会在dist/apps目录下生成asset-app.jar文件。确保节点处于启动状态,进入到dist目录,运行该程序:

bash asset_run.sh deploy

运行结果为:

区块链供应链金融实战2_FiscoBcos


创建两个测试账户:

bash asset_run.sh register alice007 100
bash asset_run.sh register bob007 200

区块链供应链金融实战2_FiscoBcos_02


账户间转账:

bash asset_run.sh transfer bob007 alice007 80

区块链供应链金融实战2_金链盟_03


查询账户余额:

bash asset_run.sh query alice007

区块链供应链金融实战2_bc_04


如果大家可以成功复制上述结果,我们就可以开始下一步开发工作了。官方给的示例是一个独立的程序,而我们实际的应用场景中,我们通常需要在Web服务器中调用智能合约,来完成相应的业务逻辑。因此我们下一步的任务就是将这个工程改造为SpringBoot工程,将deploy、register、transfer操作变为相应的RESTful请求。

为了使用智能合约,我们需要定义一个可以调用Cbdc智能合约的工具类,如下所示:

package com.arxandt.scf.fbc.sol.client;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.util.List;
import java.util.Properties;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.fisco.bcos.channel.client.Service;
import org.fisco.bcos.web3j.crypto.Credentials;
import org.fisco.bcos.web3j.crypto.Keys;
import org.fisco.bcos.web3j.protocol.Web3j;
import org.fisco.bcos.web3j.protocol.channel.ChannelEthereumService;
import org.fisco.bcos.web3j.protocol.core.methods.response.TransactionReceipt;
import org.fisco.bcos.web3j.tuples.generated.Tuple2;
import org.fisco.bcos.web3j.tx.gas.StaticGasProvider;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;

import com.arxandt.scf.fbc.sol.contract.Cbdc;
import com.arxandt.scf.fbc.sol.contract.Cbdc.RegisterEventEventResponse;
import com.arxandt.scf.fbc.sol.contract.Cbdc.TransferEventEventResponse;

public class CbdcClient {

static Logger logger = LoggerFactory.getLogger(CbdcClient.class);

private Web3j web3j;

private Credentials credentials;

public Web3j getWeb3j() {
return web3j;
}

public void setWeb3j(Web3j web3j) {
this.web3j = web3j;
}

public Credentials getCredentials() {
return credentials;
}

public void setCredentials(Credentials credentials) {
this.credentials = credentials;
}

public final static String CONTRACT_FILE = "/home/yantao/scf/contract.properties";
/**
* for CBDC contract
*
* */
public void recordCbdcAddr(String address) throws FileNotFoundException, IOException {
Properties prop = new Properties();
prop.setProperty("address", address);
FileOutputStream fileOutputStream = new FileOutputStream(CbdcClient.CONTRACT_FILE, false);
prop.store(fileOutputStream, "contract address");
System.out.println("CbdcClient.recordCbdcAddr 4");
}

/**
* For CBDC contract
*/
public String loadCbdcAddr() throws Exception {
// load Cbdc contact address from contract.properties
Properties prop = new Properties();
prop.load(new FileInputStream(CbdcClient.CONTRACT_FILE));
String contractAddress = prop.getProperty("address");
if (contractAddress == null || contractAddress.trim().equals("")) {
throw new Exception(" load Cbdc contract address failed, please deploy it first. ");
}

logger.info(" load Cbdc address from contract.properties, address is {}", contractAddress);
return contractAddress;
}

public void initialize() throws Exception {
// init the Service
@SuppressWarnings("resource")
ApplicationContext context = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
Service service = context.getBean(Service.class);
service.run();
ChannelEthereumService channelEthereumService = new ChannelEthereumService();
channelEthereumService.setChannelService(service);
Web3j web3j = Web3j.build(channelEthereumService, 1);
// init Credentials
Credentials credentials = Credentials.create(Keys.createEcKeyPair());
setCredentials(credentials);
setWeb3j(web3j);
logger.debug(" web3j is " + web3j + " ,credentials is " + credentials);
}

private static BigInteger gasPrice = new BigInteger("30000000");
private static BigInteger gasLimit = new BigInteger("30000000");

public void deployCbdcAndRecordAddr() {
try {
Cbdc cbdc = Cbdc.deploy(web3j, credentials, new StaticGasProvider(gasPrice, gasLimit)).send();
System.out.println(" deploy CBDC success, contract address is " + cbdc.getContractAddress());

recordCbdcAddr(cbdc.getContractAddress());
} catch (Exception e) {
System.out.println(" deploy CBDC contract failed, error message is " + e.getMessage());
}
}


public BigInteger queryCbdcAmount(String cbdcAccount) {
BigInteger balance = new BigInteger("0");
try {
String contractAddress = loadCbdcAddr();

Cbdc cbdc = Cbdc.load(contractAddress, web3j, credentials, new StaticGasProvider(gasPrice, gasLimit));
Tuple2<BigInteger, BigInteger> result = cbdc.select(cbdcAccount).send();
balance = result.getValue2();
if (result.getValue1().compareTo(new BigInteger("0")) == 0) {
System.out.printf(" cbdc account %s, value %s \n", cbdcAccount, result.getValue2());
} else {
System.out.printf(" %s cbdc account is not exist \n", cbdcAccount);
}
} catch (Exception e) {
logger.error(" queryCbdcAmount exception, error message is {}", e.getMessage());
System.out.printf(" query cbdc account failed, error message is %s\n", e.getMessage());
}
return balance;
}

public void registerCbdcAccount(String cbdcAccount, BigInteger amount) {
try {
String contractAddress = loadCbdcAddr();
Cbdc cbdc = Cbdc.load(contractAddress, web3j, credentials, new StaticGasProvider(gasPrice, gasLimit));
TransactionReceipt receipt = cbdc.register(cbdcAccount, amount).send();
List<RegisterEventEventResponse> response = cbdc.getRegisterEventEvents(receipt);
if (!response.isEmpty()) {
if (response.get(0).ret.compareTo(new BigInteger("0")) == 0) {
System.out.printf(" register cbdc account success => cbdc: %s, value: %s \n", cbdcAccount,
amount);
} else {
System.out.printf(" register cbdc account failed, ret code is %s \n",
response.get(0).ret.toString());
}
} else {
System.out.println(" event log not found, maybe transaction not exec. ");
}
} catch (Exception e) {
logger.error(" registerCbdcAccount exception, error message is {}", e.getMessage());
System.out.printf(" register cbdc account failed, error message is %s\n", e.getMessage());
}
}

public void transferCbdc(String fromCbdcAccount, String toCbdcAccount, BigInteger amount) {
try {
String contractAddress = loadCbdcAddr();
Cbdc cbdc = Cbdc.load(contractAddress, web3j, credentials, new StaticGasProvider(gasPrice, gasLimit));
TransactionReceipt receipt = cbdc.transfer(fromCbdcAccount, toCbdcAccount, amount).send();
List<TransferEventEventResponse> response = cbdc.getTransferEventEvents(receipt);
if (!response.isEmpty()) {
if (response.get(0).ret.compareTo(new BigInteger("0")) == 0) {
System.out.printf(" transfer success => from_cbdc: %s, to_cbdc: %s, amount: %s \n",
fromCbdcAccount, toCbdcAccount, amount);
} else {
System.out.printf(" transfer cbdc account failed, ret code is %s \n",
response.get(0).ret.toString());
}
} else {
System.out.println(" event log not found, maybe transaction not exec. ");
}
} catch (Exception e) {
logger.error(" registerCbdcAccount exception, error message is {}", e.getMessage());
System.out.printf(" register cbdc account failed, error message is %s\n", e.getMessage());
}
}
}

这里与官方示例有一点不同,首先不需要main方法,其次官网记录智能合约地址的文件在类目录下,而我们要用runnable jar的形式,所以将该文件写到普通文件中。
我们首先定义一个SpringBoot工程的Application类,如下所示:

package com.arxandt.scf.fbc;

import java.util.Arrays;

import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}

@Bean
public CommandLineRunner commandLineRunner(ApplicationContext ctx) {
return args -> {

System.out.println("Let's inspect the beans provided by Spring Boot:");

String[] beanNames = ctx.getBeanDefinitionNames();
Arrays.sort(beanNames);
for (String beanName : beanNames) {
System.out.println(beanName);
}
};
}
}

这个与标准的SpringBoot工程的Application没有区别,就不做介绍了。
接下来我们定义controller类,如下所示:

package com.arxandt.scf.fbc.controller;
// java lib
import java.math.BigInteger;
// open source lib
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMethod;

import com.arxandt.scf.fbc.sol.client.CbdcClient;

@RestController
@RequestMapping("/test")
public class TestController {
private static boolean hasCbdcDeployed = false;
private static CbdcClient cc = null;

@RequestMapping("/")
public String index() {
return "Greetings from Spring Boot! Yantao";
}

@RequestMapping(value="/fbc/{accountTypeId}", method=RequestMethod.GET)
public String getAccountType(@PathVariable String accountTypeId, HttpServletRequest req) {
String accountTypeName = "";
try {
if (!TestController.hasCbdcDeployed) {
cc = new CbdcClient();
cc.initialize();
cc.deployCbdcAndRecordAddr();
TestController.hasCbdcDeployed = true;
}
//accountTypeName = cc.getAccountTypeName(accountTypeId);
} catch (Exception ex) {
System.out.println("exception:" + ex.getMessage() + "!");
}
return "ok?????? accountTypeId=" + accountTypeId + "; rid=" + req.getParameter("accountTypeId") + "; name=" + accountTypeName + "!";
}

@RequestMapping(value="/fbc/register/{account}/{amount}", method=RequestMethod.GET)
public String registerAccount(@PathVariable String account, @PathVariable String amount) {
System.out.println("account=" + account + "; amount=" + amount + "!");
try {
if (!TestController.hasCbdcDeployed) {
cc = new CbdcClient();
cc.initialize();
cc.deployCbdcAndRecordAddr();
TestController.hasCbdcDeployed = true;
}
cc.registerCbdcAccount(account, new BigInteger(amount));
} catch (Exception ex) {
System.out.println("registerAccount Exception:" + ex.getMessage() + "!");
}
return "registerAccount Ok";
}

@RequestMapping(value="/fbc/query/{account}", method=RequestMethod.GET)
public String queryAccount(@PathVariable String account) {
BigInteger balance = new BigInteger("0");
try {
if (!TestController.hasCbdcDeployed) {
cc = new CbdcClient();
cc.initialize();
cc.deployCbdcAndRecordAddr();
TestController.hasCbdcDeployed = true;
}
balance = cc.queryCbdcAmount(account);
} catch (Exception ex) {
System.out.println("queryAccount Exception:" + ex.getMessage() + "!");
}
return "amount=" + balance + "!";
}

@RequestMapping(value="/fbc/transfer/{fromAccount}/{toAccount}/{amount}", method=RequestMethod.GET)
public String transferAccount(@PathVariable String fromAccount, @PathVariable String toAccount, @PathVariable String amount) {
BigInteger amountI = new BigInteger(amount);
try {
if (!TestController.hasCbdcDeployed) {
cc = new CbdcClient();
cc.initialize();
cc.deployCbdcAndRecordAddr();
TestController.hasCbdcDeployed = true;
}
cc.transferCbdc(fromAccount, toAccount, amountI);
} catch (Exception ex) {
System.out.println("transferAccount Exception:" + ex.getMessage() + "!");
}
return "transfer is Ok";
}
}

最后我们需要修改build.gradle文件,使其可以编译整个工程:

buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:2.2.2.RELEASE")
}
}

apply plugin: 'maven'
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

bootJar {
baseName = 'gs-spring-boot'
version = '0.1.0'
}


sourceCompatibility = 1.8
targetCompatibility = 1.8

[compileJava, compileTestJava, javadoc]*.options*.encoding = 'UTF-8'

// In this section you declare where to find the dependencies of your project
repositories {
maven {
url "http://maven.aliyun.com/nexus/content/groups/public/"
}
maven { url "https://dl.bintray.com/ethereum/maven/" }
maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
mavenCentral()
}


List logger = [
'org.slf4j:slf4j-log4j12:1.7.25'
]

// In this section you declare the dependencies for your production and test code
dependencies {
compile logger
runtime logger
compile ("org.fisco-bcos:web3sdk:2.1.0")
compile("org.springframework.boot:spring-boot-starter-web")
testCompile("junit:junit")
}

configurations {
//eliminates logback
all*.exclude group: 'ch.qos.logback'

//eliminates StackOverflowError
all*.exclude group: 'org.apache.logging.log4j', module: 'log4j-to-slf4j'
}

jar {
destinationDir file('dist/apps')
archiveName project.name + '.jar'
exclude '**/*.xml'
exclude '**/*.properties'
exclude '**/*.crt'
exclude '**/*.key'

doLast {
copy {
from configurations.runtime
into 'dist/lib'
}
copy {
from file('src/test/resources/')
into 'dist/conf'
}
copy {
from file('tool/')
into 'dist/'
}
copy {
from file('src/test/resources/contract')
into 'dist/contract'
}
}
}

我们运行如下命令编译整个工程并运行程序:

./gradlew build
java -Djdk.tls.namedGroups="secp256k1" -jar build/libs/gs-spring-boot-0.1.0.jar

启动之后,在浏览器中输入如下网址:

# 部署智能合约
http://your.ip.addr:8080/test/fbc/deploy
# 创建账户
http://your.ip.addr:8080/test/fbc/register/alice009/100
http://your.ip.addr:8080/test/fbc/register/bob009/200
# 转账
http://your.ip.addr:8080/test/fbc/transfer/bob009/alice009/20
# 查询账户余额
http://your.ip.addr:8080/test/fbc/query/alice009
http://your.ip.addr:8080/test/fbc/query/bob009

上述操作应该可以产生与命令行相同的结果。
在本篇博文中,我们实现了一个简单的智能合约,并部署到Fisco Boco中,并利用基于SpringBoot的Web服务器,来调用智能合约。下面的博文中,我们将先讲解区块链供应链金融业务知识,然后来讲解怎样通过Fisco Bcos智能合约来实现这些业务逻辑,最后讲解怎样通过SpringBoot和Element-Admin-Ui来提供最终用户操作界面。


【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月08日 0

暂无评论

推荐阅读
oCSbu1MUxU1k