应用程序开发 - 智能合约处理
区块链网络的核心是智能合约。在 PaperNet 中,商业票据智能合约中的代码定义了商业票据的有效状态,以及将票据从一种状态转换为另一种状态的交易逻辑。在本主题中,我们将向你展示如何实施一个现实世界的智能合约,该合约管理着发行,购买和赎回商业票据的过程。
我们将介绍:
- 什么是智能合约及其重要性
- 如何定义智能合约
- 如何定义交易
- 如何进行交易
- 如何在智能合约中表示业务对象
- 如何在帐本中存储和检索对象
如果需要,你可以 下载示例,甚至可以在 本地运行。它是用 JavaScript 和 Java 编写的,但是逻辑是完全独立于语言的,因此你可以轻松地看到发生了什么!(该示例也将适用于 Go。)
1. 智能合约
智能合约定义业务对象的不同状态,并管理在这些不同状态之间移动对象的流程。智能合约之所以重要,是因为它们使架构师和智能合约开发人员能够定义关键业务流程和数据,这些关键业务流程和数据是在区块链网络中进行协作的不同组织之间共享的。
在 PaperNet 网络中,智能合约由 MagnetoCorp 和 DigiBank 等不同的网络参与者共享。连接到网络的所有应用程序必须使用相同版本的智能合约,以便它们共同实现相同的共享业务流程和数据。
2. 实现语言
支持两个运行时,即 Java 虚拟机和 Node.js。这使你有机会使用 JavaScript,TypeScript,Java 或可以在这些受支持的运行时之一中运行的任何其他语言之一。
在 Java 和 TypeScript 中,注释或修饰符用于提供有关智能合约及其结构的信息。这样可以提供更丰富的开发经验-例如,可以强制执行作者信息或返回类型。在 JavaScript 中,必须遵循约定,因此,围绕自动确定的内容存在限制。
JavaScript 和 Java 都给出了示例。
3. 合约类
PaperNet 商业票据智能合约的副本包含在一个文件中。使用浏览器查看它,如果已下载,则在你喜欢的编辑器中将其打开。
-
papercontract.js
- JavaScript version -
CommercialPaperContract.java
- Java version
你可能会从文件路径中注意到,这是 MagnetoCorp 的智能合约副本。 MagnetoCorp 和 DigiBank 必须就他们将要使用的智能合约的版本达成协议。目前,使用哪个组织的副本都没关系,它们都是一样的。
花一些时间看一下智能合约的整体结构;请注意,它很短!在文件顶部,你会看到商业票据智能合约的定义:
JavaScript
class CommercialPaperContract extends Contract {...}
Java
@Contract(...)
@Default
public class CommercialPaperContract implements ContractInterface {...}
CommercialPaperContract 类包含商业票据的交易定义 - 发行,购买和赎回。正是这些交易使商业票据得以存在并在其生命周期中移动。我们将很快检查这些交易,但现在就 JavaScript 而言,CommericalPaperContract 扩展了Hyperledger Fabric Contract 类。
对于 Java,该类必须使用 @Contract(...) 注释进行修饰。这提供了提供有关合同的其他信息的机会,例如许可证和作者。@Default() 批注指示此合约类是默认合约类。能够将合约类别标记为默认合约类别在某些具有多个合约类别的智能合约中很有用。
如果你使用的是 TypeScript 实现,则有类似的 @Contract(...) 批注可以实现与 Java 中相同的目的。
有关可用注释的更多信息,请查阅可用的 API 文档:
- API documentation for Java smart contracts
- API documentation for Node.js smart contracts
这些类,批注和 Context
类在之前已引入范围:
JavaScript
const { Contract, Context } = require('fabric-contract-api');
Java
我们的商业票据合约将使用这些类的内置功能,例如自动方法调用,每个交易上下文,交易处理程序 和类共享状态。
还请注意,JavaScript 类构造函数如何使用其 超类 使用显式 合约名称 进行初始化:
constructor() {
super('org.papernet.commercialpaper');
}
对于 Java 类,构造函数为空白,因为可以在 @Contract() 注解中指定显式合约名称。如果不存在,则使用类名称。
最重要的是,org.papernet.commercialpaper
的描述性非常强 – 该智能合约是所有 PaperNet 组织对商业票据的公认定义。
通常,每个文件只有一个智能合约 – 合约往往具有不同的生命周期,因此将它们分开是明智的。但是,在某些情况下,多个智能合约可能会为应用程序提供语法帮助,例如 EuroBond,DollarBond,YenBond,但本质上提供相同的功能。在这种情况下,可以消除智能合约和交易的歧义。
4. 交易定义
在该类中,找到 issue
方法。
JavaScript
async issue(ctx, issuer, paperNumber, issueDateTime, maturityDateTime, faceValue) {...}
Java
@Transaction
public CommercialPaper issue(CommercialPaperContext ctx,
String issuer,
String paperNumber,
String issueDateTime,
String maturityDateTime,
int faceValue) {...}
Java 注解 @Transaction 用于将该方法标记为交易定义。 TypeScript 具有等效的注释。
每当调用此合约以发行商业票据时,便会控制此功能。回想一下如何通过以下交易创建商业票据 00001:
Txn = issue
Issuer = MagnetoCorp
Paper = 00001
Issue time = 31 May 2020 09:00:00 EST
Maturity date = 30 November 2020
Face value = 5M USD
我们已经更改了编程风格的变量名称,但是看到了这些属性如何几乎直接映射到 issue
方法变量。
每当应用程序请求签发商业票据时,合约都会自动授予 issue
方法控制权。交易属性值通过相应的变量可供方法使用。请参阅 应用程序主题 中的示例应用程序,了解应用程序如何使用 Hyperledger Fabric SDK 提交交易。
你可能已经注意到 issue
定义中有一个额外的变量 ctx
。它称为 交易上下文,并且始终是第一位。默认情况下,它维护与 交易逻辑 相关的按合约和按交易的信息。例如,它将包含 MagnetoCorp 的指定交易标识符,MagnetoCorp 颁发用户的数字证书以及对帐本 API 的访问。
通过实现自己的 createContext()
方法而不是接受默认实现,了解智能合约如何扩展默认交易上下文:
JavaScript
createContext() {
return new CommercialPaperContext()
}
Java
@Override
public Context createContext(ChaincodeStub stub) {
return new CommercialPaperContext(stub);
}
此扩展上下文将自定义属性 paperList
添加到默认值:
JavaScript
class CommercialPaperContext extends Context {
constructor() {
super();
// All papers are held in a list of papers
this.paperList = new PaperList(this);
}
Java
class CommercialPaperContext extends Context {
public CommercialPaperContext(ChaincodeStub stub) {
super(stub);
this.paperList = new PaperList(this);
}
public PaperList paperList;
}
我们很快将看到 ctx.paperList
随后如何用于帮助存储和检索所有 PaperNet 商业票据。
为了巩固你对智能合约交易结构的理解,找到购买和赎回交易定义,并查看是否可以看到它们如何映射到其相应的商业票据交易。
购买交易:
Txn = buy
Issuer = MagnetoCorp
Paper = 00001
Current owner = MagnetoCorp
New owner = DigiBank
Purchase time = 31 May 2020 10:00:00 EST
Price = 4.94M USD
JavaScript
async buy(ctx, issuer, paperNumber, currentOwner, newOwner, price, purchaseTime) {...}
Java
@Transaction
public CommercialPaper buy(CommercialPaperContext ctx,
String issuer,
String paperNumber,
String currentOwner,
String newOwner,
int price,
String purchaseDateTime) {...}
赎回交易
Txn = redeem
Issuer = MagnetoCorp
Paper = 00001
Redeemer = DigiBank
Redeem time = 31 Dec 2020 12:00:00 EST
JavaScript
async redeem(ctx, issuer, paperNumber, redeemingOwner, redeemDateTime) {...}
Java
@Transaction
public CommercialPaper redeem(CommercialPaperContext ctx,
String issuer,
String paperNumber,
String redeemingOwner,
String redeemDateTime) {...}
在这两种情况下,请观察商业票据交易与智能合约方法定义之间的 1:1 对应关系。
所有 JavaScript 函数都使用 async 和 await 关键字,这些关键字使 JavaScript 函数可以被视为同步函数调用。
5. 交易逻辑
既然你已经了解了合约的结构和定义的交易方式,那么让我们关注智能合约中的逻辑。
回顾第一笔 issue
交易:
JavaScript
Txn = issue
Issuer = MagnetoCorp
Paper = 00001
Issue time = 31 May 2020 09:00:00 EST
Maturity date = 30 November 2020
Face value = 5M USD
导致 issue
方法被调用:
async issue(ctx, issuer, paperNumber, issueDateTime, maturityDateTime, faceValue) {
// create an instance of the paper
let paper = CommercialPaper.createInstance(issuer, paperNumber, issueDateTime, maturityDateTime, faceValue);
// Smart contract, rather than paper, moves paper into ISSUED state
paper.setIssued();
// Newly issued paper is owned by the issuer
paper.setOwner(issuer);
// Add the paper to the list of all similar commercial papers in the ledger world state
await ctx.paperList.addPaper(paper);
// Must return a serialized paper to caller of smart contract
return paper.toBuffer();
}
Java
@Transaction
public CommercialPaper issue(CommercialPaperContext ctx,
String issuer,
String paperNumber,
String issueDateTime,
String maturityDateTime,
int faceValue) {
System.out.println(ctx);
// create an instance of the paper
CommercialPaper paper = CommercialPaper.createInstance(issuer, paperNumber, issueDateTime, maturityDateTime,
faceValue,issuer,"");
// Smart contract, rather than paper, moves paper into ISSUED state
paper.setIssued();
// Newly issued paper is owned by the issuer
paper.setOwner(issuer);
System.out.println(paper);
// Add the paper to the list of all similar commercial papers in the ledger
// world state
ctx.paperList.addPaper(paper);
// Must return a serialized paper to caller of smart contract
return paper;
}
逻辑很简单:获取交易输入变量,创建一个新的商业票据,使用 paperList 将其添加到所有商业票据的列表中,然后返回新的商业票据 (序列化为缓冲区) 作为交易响应。
了解如何从交易上下文中检索 paperList
以提供对商业票据列表的访问。 issue()
,buy()
和 redeem()
不断重新访问 ctx.paperList
,以使商业票据列表保持最新。
购买交易的逻辑更加复杂:
JavaScript
async buy(ctx, issuer, paperNumber, currentOwner, newOwner, price, purchaseDateTime) {
// Retrieve the current paper using key fields provided
let paperKey = CommercialPaper.makeKey([issuer, paperNumber]);
let paper = await ctx.paperList.getPaper(paperKey);
// Validate current owner
if (paper.getOwner() !== currentOwner) {
throw new Error('Paper ' + issuer + paperNumber + ' is not owned by ' + currentOwner);
}
// First buy moves state from ISSUED to TRADING
if (paper.isIssued()) {
paper.setTrading();
}
// Check paper is not already REDEEMED
if (paper.isTrading()) {
paper.setOwner(newOwner);
} else {
throw new Error('Paper ' + issuer + paperNumber + ' is not trading. Current state = ' +paper.getCurrentState());
}
// Update the paper
await ctx.paperList.updatePaper(paper);
return paper.toBuffer();
}
Java
@Transaction
public CommercialPaper buy(CommercialPaperContext ctx,
String issuer,
String paperNumber,
String currentOwner,
String newOwner,
int price,
String purchaseDateTime) {
// Retrieve the current paper using key fields provided
String paperKey = State.makeKey(new String[] { paperNumber });
CommercialPaper paper = ctx.paperList.getPaper(paperKey);
// Validate current owner
if (!paper.getOwner().equals(currentOwner)) {
throw new RuntimeException("Paper " + issuer + paperNumber + " is not owned by " + currentOwner);
}
// First buy moves state from ISSUED to TRADING
if (paper.isIssued()) {
paper.setTrading();
}
// Check paper is not already REDEEMED
if (paper.isTrading()) {
paper.setOwner(newOwner);
} else {
throw new RuntimeException(
"Paper " + issuer + paperNumber + " is not trading. Current state = " + paper.getState());
}
// Update the paper
ctx.paperList.updatePaper(paper);
return paper;
}
在使用 paper.setOwner(newOwner)
更改所有者之前,请查看交易如何检查 currentOwner
和 paper
是否 TRADING
。但是基本流程很简单 – 检查一些前提条件,设置新所有者,更新帐本上的商业票据,并将更新后的商业票据 (序列化为缓冲区) 作为交易响应返回。
你为什么不看看能否理解赎回交易的逻辑?
6. 表示一个对象
我们已经了解了如何使用 CommercialPaper 和 PaperList 类来定义和实现发行,购买和赎回交易。通过查看这些类的工作原理来结束本主题。
找到 CommercialPaper 类:
JavaScript
在文件 paper.js 中:
class CommercialPaper extends State {...}
Java
在文件 CommercialPaper.java 中:
@DataType()
public class CommercialPaper extends State {...}
此类包含商业票据状态的内存表示形式。查看 createInstance
方法如何使用提供的参数初始化新的商业票据:
JavaScript
static createInstance(issuer, paperNumber, issueDateTime, maturityDateTime, faceValue) {
return new CommercialPaper({ issuer, paperNumber, issueDateTime, maturityDateTime, faceValue });
}
Java
public static CommercialPaper createInstance(String issuer, String paperNumber, String issueDateTime,
String maturityDateTime, int faceValue, String owner, String state) {
return new CommercialPaper().setIssuer(issuer).setPaperNumber(paperNumber).setMaturityDateTime(maturityDateTime)
.setFaceValue(faceValue).setKey().setIssueDateTime(issueDateTime).setOwner(owner).setState(state);
}
回顾发行交易如何使用此类:
JavaScript
let paper = CommercialPaper.createInstance(issuer, paperNumber, issueDateTime, maturityDateTime, faceValue);
Java
CommercialPaper paper = CommercialPaper.createInstance(issuer, paperNumber, issueDateTime, maturityDateTime,
faceValue,issuer,"");
了解每次调用发行交易的方式,都会创建一个包含交易数据的新的商业票据内存实例。
需要注意的几个要点:
- 这是内存中的表示形式;稍后我们将看到它如何显示在帐本中。
- CommercialPaper 类扩展了 State 类。 State 是一个应用程序定义的类,它为状态创建通用抽象。所有状态都有一个它们表示的业务对象类,一个复合键,可以序列化和反序列化等等。当我们在账本上存储多个业务对象类型时,State 可以使我们的代码更易读。检查 state.js 文件 中的 State 类。
票据在创建时会计算自己的键 - 访问帐本时将使用此键。键由 issuer
和 paperNumber
组合而成。
constructor(obj) {
super(CommercialPaper.getClass(), [obj.issuer, obj.paperNumber]);
Object.assign(this, obj);
}
通过交易而不是票据类将票据移动到 ISSUED 状态。这是因为,智能合约支配着票据的生命周期状态。例如,import
交易可能会立即在 TRADING
状态下创建一组新文件。
CommercialPaper 类的其余部分包含简单的帮助程序方法:
getOwner() {
return this.owner;
}
回忆一下智能合约如何使用这种方法在商业票据的生命周期中移动。例如,在赎回交易中,我们看到:
if (paper.getOwner() === redeemingOwner) {
paper.setOwner(paper.getIssuer());
paper.setRedeemed();
}
7. 访问账本
现在,在 paperlist.js 文件 中找到 PaperList 类:
class PaperList extends StateList {
该通用程序类用于管理 Hyperledger Fabric 状态数据库中的所有 PaperNet 商业票据。 PaperList 数据结构在 体系结构主题 中有更详细的描述。
与 CommercialPaper 类类似,该类扩展了应用程序定义的 StateList 类,该类为状态列表创建通用抽象 - 在这种情况下,是 PaperNet 中的所有商业票据。
addPaper()
方法是对 StateList.addState()
方法的简单修饰:
async addPaper(paper) {
return this.addState(paper);
}
你可以在 StateList.js 文件 中看到 StateList 类如何使用 Fabric API putState() 将商业票据作为状态数据写入帐本中:
async addState(state) {
let key = this.ctx.stub.createCompositeKey(this.name, state.getSplitKey());
let data = State.serialize(state);
await this.ctx.stub.putState(key, data);
}
帐本中的每个状态数据都需要以下两个基本元素:
- 键:键是使用 createCompositeKey() 使用固定名称和
state
键形成的。名称是在构造 PaperList 对象时分配的,state.getSplitKey() 确定每个状态的唯一键。 - 数据:数据只是使用 State.serialize() 实用程序方法创建的商业票据状态的序列化形式。 State 类使用 JSON 序列化和反序列化数据,并且在构造 PaperList 对象时再次设置 State 的业务对象类,在我们的示例中为 CommercialPaper。
请注意,StateList 如何不存储有关单个状态或状态总列表的任何内容,而是将所有这些内容委派给 Fabric 状态数据库。这是一种重要的设计模式 – 减少了 Hyperledger Fabric 中 账本 MVCC 冲突 的机会。
StateList 的 getState() 和 updateState() 方法的工作方式类似:
async getState(key) {
let ledgerKey = this.ctx.stub.createCompositeKey(this.name, State.splitKey(key));
let data = await this.ctx.stub.getState(ledgerKey);
let state = State.deserialize(data, this.supportedClasses);
return state;
}
async updateState(state) {
let key = this.ctx.stub.createCompositeKey(this.name, state.getSplitKey());
let data = State.serialize(state);
await this.ctx.stub.putState(key, data);
}
了解他们如何使用 Fabric API 的 putState(),getState() 和 createCompositeKey() 来访问帐本。我们稍后将扩展此智能合约,以在 paperNet 中列出所有商业票据 - 实现此账本检索的方法将是什么样?
就是这样!在本主题中,你已经了解了如何为 PaperNet 实现智能合约。你可以转到下一个子主题,以查看应用程序如何使用 Fabric SDK 调用智能合约。
Reference
- Docs » Developing Applications » Smart Contract Processing, https://hyperledger-fabric.readthedocs.io/en/release-1.4/developapps/smartcontract.html
- Docs » Getting Started » Install Samples, Binaries and Docker Images, https://hyperledger-fabric.readthedocs.io/en/release-1.4/install.html
- Docs » Tutorials » Commercial paper tutorial, https://hyperledger-fabric.readthedocs.io/en/release-1.4/tutorial/commercial_paper.html
- https://fabric-shim.github.io/release-1.4/fabric-contract-api.Contract.html
- https://hyperledger.github.io/fabric-chaincode-java/
- https://fabric-shim.github.io/release-1.4/index.html
- Docs » Developing Applications » Application design elements » Transaction context, https://hyperledger-fabric.readthedocs.io/en/release-1.4/developapps/transactioncontext.html
- Docs » Developing Applications » Application design elements » Transaction handlers, https://hyperledger-fabric.readthedocs.io/en/release-1.4/developapps/transactionhandler.html
- Docs » Developing Applications » Application design elements » Contract names, https://hyperledger-fabric.readthedocs.io/en/release-1.4/developapps/contractname.html
- Docs » Developing Applications » Process and Data Design, https://hyperledger-fabric.readthedocs.io/en/release-1.4/developapps/architecture.html
- Docs » Architecture Reference » Read-Write set semantics, https://hyperledger-fabric.readthedocs.io/en/release-1.4/readwrite.html
项目源代码
项目源代码会逐步上传到 Github,地址为 https://github.com/windstamp。
Contributor
- Windstamp, https://github.com/windstamp