區塊鏈供應鏈金融實戰2

在本篇博文中,我們將講解在金鍊盟下,編寫一個最簡單的用戶間轉賬的智能合約,並通過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

運行結果爲:
部署智能合約
創建兩個測試賬戶:

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

創建測試賬戶
賬戶間轉賬:

bash asset_run.sh transfer bob007 alice007 80

賬戶間轉賬
查詢賬戶餘額:

bash asset_run.sh query alice007

查詢賬戶餘額
如果大家可以成功複製上述結果,我們就可以開始下一步開發工作了。官方給的示例是一個獨立的程序,而我們實際的應用場景中,我們通常需要在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來提供最終用戶操作界面。

發佈了204 篇原創文章 · 獲贊 1236 · 訪問量 124萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章