Smart Contract Processing

受众:架构师、应用程序和聪智能合约开发者

在区块链网络的核心是一个智能合约。在PaperNet中,商业票据智能合约中的代码定义了商业票据的有效状态,以及将票据从一种状态转换到另一种状态的交易逻辑。在本主题中,我们将向您展示如何实现一个管理商业票据发行、购买和赎回过程的真实世界智能合约。

我们会讲到:

如果您愿意,可以下载示例,甚至在本地运行它。它是用JavaScript编写的,但是逻辑是完全独立于语言的,所以您可以很容易地看到发生了什么!(Java和GOLANG也将提供该示例。)

Smart Contract

智能合约定义业务对象的不同状态,并管理对象在这些不同状态之间移动的流程。智能合约非常重要,因为它们允许架构师和智能合约开发者定义关键业务流程和数据,在区块链网络中跨组织协作。

在PaperNet网络中,智能合约由不同的网络参与者共享,如MagnetoCorp和DigiBank。所有连接到网络的应用程序都必须使用相同版本的智能合约,以便它们共同实现相同的共享业务流程和数据。

Contract class

PaperNet商业票据智能合约的副本包含在papercontract.js中。用你的浏览器查看它,或者在你最喜欢的编辑器中打开它(如果你已经下载了它)。

您可能从文件路径中注意到,这是MagnetoCorp的智能合约副本。MagnetoCorp和DigiBank必须就它们将使用的智能合约版本达成一致。现在,不管你看的是哪个组织的副本,它们都是一样的。

花点时间看看智能合约的整体结构;注意,它相当短!在 papercontract.js的顶部,你会看到商业票据智能合约有一个定义:

class CommercialPaperContract extends Contract {...}

CommercialPaperContract类包含商业票据的交易定义——发行、购买和赎回。正是这些交易使商业票据得以存在,并在其生命周期中移动它们。我们将很快研究这些交易,但现在请注意 CommercialPaperContract是如何扩展超级账本Fabric Contract类的。这个内置类和上下文类在前面已经介绍过:

const { Contract, Context } = require('fabric-contract-api');

我们的商业票据合约将使用这些类的内置特性,例如自动方法调用、每个交易上下文、交易处理程序和类共享状态。

还请注意类构造函数如何使用它的超类用显式的合约名初始化自己:

constructor() {
    super('org.papernet.commercialpaper');
}

最重要的是,org.papernet.commercialpaper非常具有描述性——这个智能合约是所有PaperNet组织对商业票据的一致定义。

通常每个文件只有一个智能合约——合约往往有不同的生命周期,因此将它们分开是明智的。然而,在某些情况下,多个智能合约可能为应用程序提供语法帮助,例如EuroBond、DollarBond、YenBond,但本质上提供相同的功能。在这种情况下,可以消除智能合约和交易的歧义。

Transaction definition

在类中,找到issue方法。

async issue(ctx, issuer, paperNumber, issueDateTime, maturityDateTime, faceValue) {...}

每当调用此合约以发行商业票据时,都会对该函数进行控制。回想一下商业票据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方法控制权。交易属性值通过相应的变量提供给方法。查看一个应用如何使用示例应用程序,在应用主题中使用超级账本Fabric SDK提交交易。

您可能已经注意到了issue定义中的一个额外变量——ctx。它被称为交易上下文,它总是第一个。默认情况下,它同时维护与交易逻辑相关的每个合约和每个交易的信息。例如,它将包含MagnetoCorp指定的交易标识符、MagnetoCorp发行的用户数字证书、以及对账本API的访问。

查看智能合约如何通过实现自己的createContext()方法扩展默认交易上下文,而不是接受默认实现:

createContext() {
  return new CommercialPaperContext()
}

此扩展上下文将自定义属性paperList添加到默认值:

class CommercialPaperContext extends Context {

  constructor() {
    super();
    // All papers are held in a list of papers
    this.paperList = new PaperList(this);
}

我们很快就会看到ctx.paperList随后可用于帮助存储和检索所有PaperNet商业票据。

要巩固对智能合约交易结构的理解,请找到购买和赎回交易定义,并查看它们如何映射到相应的商业票据交易。

购买交易:

async buy(ctx, issuer, paperNumber, currentOwner, newOwner, price, purchaseTime) {...}
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

赎回交易:

async redeem(ctx, issuer, paperNumber, redeemingOwner, redeemDateTime) {...}
Txn = redeem
Issuer = MagnetoCorp
Paper = 00001
Redeemer = DigiBank
Redeem time = 31 Dec 2020 12:00:00 EST

在这两种情况下,观察商业票据交易与智能合约方法定义之间的1:1对应关系。不要担心异步,等待关键字——它们允许将异步JavaScript函数像其他编程语言中的同步函数一样对待。

Transaction logic

现在您已经了解了合约的结构和交易的定义,让我们将重点放在智能合约中的逻辑上。

回想第一个发行交易:

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();
}

逻辑很简单:获取交易输入变量,创建一个新的商业票据,使用paperList将其添加到所有商业票据的列表中,并返回新的商业票据(作为缓冲序列化)作为交易响应。

请参阅如何从交易上下文检索paperList以提供对商业票据列表的访问。 issue()、buy() 和redeem() 不断地重新访问ctx.paperList,以保持最新的商业票据清单。

购买交易的逻辑更为复杂:

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();
}

在使用paper. setowner (newOwner)更改所有者之前,查看交易怎样检查currentOwner和正在交易的票据是否一致。基本流程很简单——检查一些先决条件,设置新所有者,更新账本上的商业票据,并将更新后的商业票据(序列化为缓冲)作为交易响应返回。

为什么不看看您是否理解赎回交易的逻辑?

Representing an object

我们已经了解了如何使用CommercialPaper和PaperList类定义和实现发行、购买和赎回交易。让我们通过了解这些类如何工作来结束这个主题。

在paper.js文件中找到CommercialPaper类:

class CommercialPaper extends State {...}

该类包含商业票据状态的内存表示形式。查看createInstance方法如何使用提供的参数初始化新的商业票据:

static createInstance(issuer, paperNumber, issueDateTime, maturityDateTime, faceValue) {
  return new CommercialPaper({ issuer, paperNumber, issueDateTime, maturityDateTime, faceValue });
}

回想一下这个类是如何被发行交易使用的:

let paper = CommercialPaper.createInstance(issuer, paperNumber, issueDateTime, maturityDateTime, faceValue);

查看每次调用发行交易时,如何创建包含交易数据的新的商业票据内存实例。

以下几点需要注意:

  • This is an in-memory representation; we’ll see later how it appears on the ledger.

  • The CommercialPaper class extends the State class. State is an application-defined class which creates a common abstraction for a state. All states have a business object class which they represent, a composite key, can be serialized and de-serialized, and so on. State helps our code be more legible when we are storing more than one business object type on the ledger. Examine the State class in the state.js file.

  • A paper computes its own key when it is created – this key will be used when the ledger is accessed. The key is formed from a combination of issuer and paperNumber.

    constructor(obj) {
      super(CommercialPaper.getClass(), [obj.issuer, obj.paperNumber]);
      Object.assign(this, obj);
    }
    
  • A paper is moved to the ISSUED state by the transaction, not by the paper class. That’s because it’s the smart contract that governs the lifecycle state of the paper. For example, an import transaction might create a new set of papers immediately in the TRADING state.

CommercialPaper类的其余部分包含简单的辅助方法:

getOwner() {
    return this.owner;
}

回想一下智能合约如何使用这样的方法来移动商业票据通过生命周期。例如,在赎回交易中我们看到:

if (paper.getOwner() === redeemingOwner) {
  paper.setOwner(paper.getIssuer());
  paper.setRedeemed();
}

Access the ledger

现在在paperlist.js文件中找到PaperList类:

class PaperList extends StateList {

这个实用程序类用于管理超级账本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()组成,使用固定的名称和状态键。构造PaperList对象时分配了名称,state. getsplitkey()确定每个状态的唯一键。 - 数据:数据只是商业票据状态的序列化形式,使用State.serialize()实用程序方法创建。State类使用JSON序列化和反序列化数据,State的业务对象类(在我们的例子中是CommercialPaper)根据需要在构造PaperList对象时再次设置。

  • Key: key is formed with createCompositeKey() using a fixed name and the key of state. The name was assigned when the PaperList object was constructed, and state.getSplitKey() determines each state’s unique key.

  • Data: data is simply the serialized form of the commercial paper state, created using the State.serialize() utility method. The State class serializes and deserializes data using JSON, and the State’s business object class as required, in our case CommercialPaper, again set when the PaperList object was constructed.

请注意,StateList不存储关于单个状态或状态总列表的任何信息——它将所有这些委托给Fabric状态数据库。这是一个重要的设计模式——它减少了在超级账本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调用智能合约。