# Исполнение смарт-контрактов **Аудитория**: Архитекторы приложений, разработчики смарт-контрактов и приложений В самом центре концепции блокчейн-сети располагается смарт-контракт. В сети PaperNet программный код смарт-контракта определяет разрешенные состояния коммерческой ценной бумаги и логику транзакции, которая переводит бумагу из одного состояния в другое. В этой главе мы покажем, как реализовать смарт-контракт для условий реального мира так, чтобы этот смарт-контракт заведовал процессами выпуска, покупки и погашения коммерческих ценных бумаг. Далее рассмотрим следующее: * [Что такое смарт-контракт, и почему он важен](#smart-contract) * [Как определить смарт-контракт](#contract-class) * [Как определить транзакцию](#transaction-definition) * [Как реализовать транзакцию](#transaction-logic) * [Как задать представление бизнес-объекта внутри смарт-контракта](#representing-an-object) * [Как хранить и как извлекать объект из реестра](#access-the-ledger) По желанию можете [загрузить пример](../install.html) и даже попробовать [запустить его на локальной машине](../tutorial/commercial_paper.html). Он написан на JavaScript и Java, но его логика не зависит от структуры языка, так что вам сразу будет ясно, что происходит (кстати, пример есть и на языке Go). ## Смарт-контракт Смарт-контрактом определяются различные состояния бизнес-объекта и регламентируются процессы, которые проводят объект от одного состояния к другому. Смарт-контракты важны, так как они позволяют архитекторам и разработчикам смарт-контрактов задавать ключевые бизнес-процессы и определять данные, которыми совместно будут пользоваться различные организации - партнеры по блокчейн-сети. В сети PaperNet, к примеру, смарт-контракт совместно используется разными участниками сети, такими как MagnetoCorp и DigiBank. Все приложения, присоединенные к сети, должны использовать одну и ту же версию смарт-контракта, чтобы совместно реализовать идентичные совместно используемые бизнес-процессы и данные. ## Языки реализации Поддерживаются две среды исполнения - Java Virtual Machine и Node.js. Таким образом, дается возможность для использования любого из языков, поддерживающихся в этой среде, будь то JavaScript, TypeScript, Java или другие. В языках Java and TypeScript, для описания информации о смарт-контракте и его структуре используются аннотации и декораторы. С их помощью разработка становится содержательнее - к примеру, можно ввести информацию об авторе или типах возвращаемых значений. Также в JavaScript необходимо следовать конвенциям (соглашениям), и, следовательно, в связи с этим есть ограничения на автоматические определения. Приведем примеры на JavaScript и Java. ## Класс контракта Копия смарт-контракта коммерческой ценной бумаги PaperNet содержится в единственном файле. Просмотрите его своим браузером или откройте в текстовом редакторе на ваш выбор. - `papercontract.js` - [версия JavaScript](https://github.com/hyperledger/fabric-samples/blob/{BRANCH}/commercial-paper/organization/magnetocorp/contract/lib/papercontract.js) - `CommercialPaperContract.java` - [версия Java](https://github.com/hyperledger/fabric-samples/blob/{BRANCH}/commercial-paper/organization/magnetocorp//contract-java/src/main/java/org/example/CommercialPaperContract.java) Легко заметить, что путь файла указывает на то, что эта копия смарт-контракта принадлежит MagnetoCorp. MagnetoCorp и DigiBank должны иметь согласованную версию смарт-контракта. В настоящий момент не важно, чья копия используется - они одинаковые. Ненадолго бросьте взгляд на полную структуру смарт-контракта - она довольно короткая. В самом верху файла вы можете заметить определение смарт-контракта коммерческих ценных бумаг:
JavaScript ```JavaScript class CommercialPaperContract extends Contract {...} ```
Java ```Java @Contract(...) @Default public class CommercialPaperContract implements ContractInterface {...} ```
Класс `CommercialPaperContract` содержит определения транзакций для коммерческих ценных бумаг - **выпустить**, **купить** и **погасить**. Именно эти транзакции создают ценную бумагу и проводят её по ряду состояний в течение её жизненного цикла. Скоро мы изучим эти [транзакции](#transaction-definition), а сейчас в случае JavaScript заметим, что `CommericalPaperContract` наследует [класс](https://hyperledger.github.io/fabric-chaincode-node/{BRANCH}/api/fabric-contract-api.Contract.html) `Contract` из Hyperledger Fabric. В языке Java, класс должен быть снабжен аннотацией `@Contract(...)`. Этот дает возможность предоставить дополнительную информацию о контракте, такую как, например, номер лицензии и имя автора. Аннотация `@Default()` сигнализирует о том, что этот класс контракта является заданным по умолчанию. Обозначить класс контракта "по умолчанию" иногда полезно в смарт-контрактах, у которых имеется множество классов. Если вы используете реализацию в TypeScript implementation, то - есть похожие аннотации `@Contract(...)`, которые служат тем же целям, что и в Java. Более подробно о возможных аннотациях можно посмотреть в документации API: * [API-документация для смарт-контрактов на Java](https://hyperledger.github.io/fabric-chaincode-java/) * [API-документация для смарт-контрактов Node.js](https://hyperledger.github.io/fabric-chaincode-node/) Классы контрактов для Fabric присутствуют и в смарт-контрактах, написанных на Go. Сейчас мы не будем обсуждать API контрактов на Go, но для них используются те же концепции, и что и в API для Java и JavaScript: * [API-документация для смарт-контрактов на Go](https://github.com/hyperledger/fabric-contract-api-go) Эти классы, аннотации и класс `Context` мы рассмотрели ранее:
JavaScript ```JavaScript const { Contract, Context } = require('fabric-contract-api'); ```
Java ```Java import org.hyperledger.fabric.contract.Context; import org.hyperledger.fabric.contract.ContractInterface; import org.hyperledger.fabric.contract.annotation.Contact; import org.hyperledger.fabric.contract.annotation.Contract; import org.hyperledger.fabric.contract.annotation.Default; import org.hyperledger.fabric.contract.annotation.Info; import org.hyperledger.fabric.contract.annotation.License; import org.hyperledger.fabric.contract.annotation.Transaction; ```
Наш контракт для коммерческих ценных бумаг использует встроенные свойства классов, такие как автоматический вызов метода, [потранзакционный контекст](./transactioncontext.html), [обработчики транзакций](./transactionhandler.html), и разделяемое классами состояние. Обратите внимание на то, как конструктор классов JavaScript использует [родительский класс](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/super), чтобы инициализироваться конкретным [названием контракта](./contractname.html): ```JavaScript constructor() { super('org.papernet.commercialpaper'); } ``` В классе Java конструктор является пустым, так как явное название контракта может быть задано в аннотации `@Contract()`. Если имя отсутствует, тогда используется название класса. Но важнейшим и наиболее подробным по описанию является `org.papernet.commercialpaper` -- этот смарт-контракт является согласованным определением ценной бумаги для всех организаций сети PaperNet. Обычно на один файл приходится один смарт-контракт - у контрактов может быть различный жизненный цикл, и поэтому разумно контракты развести по разным файлам. Но при этом в некоторых случаях, множественность смарт-контрактов и их названий может синтаксически облегчать жизнь приложениями, например, такими названиями как `EuroBond`, `DollarBond`, `YenBond`, хоть и выполняя по сути, одну и ту же функцию. Это пример тех случаев, когда постарались различать смарт-контракты и транзакции, так, чтобы в них не запутаться. ## Как определить транзакцию Внутри класса найдите метод **выпустить** (issue).
JavaScript ```JavaScript async issue(ctx, issuer, paperNumber, issueDateTime, maturityDateTime, faceValue) {...} ```
Java ```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` автоматически передается управление контрактом, как только приложение запрашивает выпуск коммерческой ценной бумаги. Значения свойств транзакции передаются методу через соответствующие переменные. В главе [приложение](./application.html) описано, как приложение отправляет транзакцию при помощи Hyperledger Fabric SDK, используя пример приложения. Вы могли заметить лишнюю переменную в определении **выпуска** -- `ctx`. Она называется [**транзакционный контекст**](./transactioncontext.html) и всегда указывается первой. По умолчанию, она содержит [транзакционную логику](#transaction-logic) для каждого контракта и каждой транзакции. Например, в ней содержится идентификатор конкретной транзакции MagnetoCorp, цифровой сертификат пользователя-эмитента в составе MagnetoCorp, а также доступ к API реестра. Взгляните, как смарт-контракт дополняет заданный по умолчанию транзакционный контекст путем реализации собственного метода `createContext()`, а не использует реализацию, данную по умолчанию:
JavaScript ```JavaScript createContext() { return new CommercialPaperContext() } ```
Java ```Java @Override public Context createContext(ChaincodeStub stub) { return new CommercialPaperContext(stub); } ```
Этот дополненный контекст добавляет свойство `paperList` к свойствам, данным по умолчанию:
JavaScript ```JavaScript class CommercialPaperContext extends Context { constructor() { super(); // Все ценные бумаги входят в список ценных бумаг this.paperList = new PaperList(this); } ```
Java ```Java class CommercialPaperContext extends Context { public CommercialPaperContext(ChaincodeStub stub) { super(stub); this.paperList = new PaperList(this); } public PaperList paperList; } ```
Впоследствии опишем, как `ctx.paperList` можно использовать для хранения и извлечения всех коммерческих ценных бумаг сети PaperNet. Чтобы укрепить понимание структуры транзакции смарт-контрактов, посмотрите на определения транзакций **buy** и **redeem**, и попробуйте понять, как они отражаются на соответствующие транзакции коммерческой ценной бумаги. Транзакция **buy** (купить): ``` 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 ```JavaScript async buy(ctx, issuer, paperNumber, currentOwner, newOwner, price, purchaseTime) {...} ```
Java ```Java @Transaction public CommercialPaper buy(CommercialPaperContext ctx, String issuer, String paperNumber, String currentOwner, String newOwner, int price, String purchaseDateTime) {...} ```
Транзакция **redeem** (погасить): ``` Txn = redeem Issuer = MagnetoCorp Paper = 00001 Redeemer = DigiBank Redeem time = 31 Dec 2020 12:00:00 EST ```
JavaScript ```JavaScript async redeem(ctx, issuer, paperNumber, redeemingOwner, redeemDateTime) {...} ```
Java ```Java @Transaction public CommercialPaper redeem(CommercialPaperContext ctx, String issuer, String paperNumber, String redeemingOwner, String redeemDateTime) {...} ```
В обоих случаях обратите внимание на взаимно-однозначное соответствие транзакции коммерческой ценной бумаги и определения смарт-контракта. Все функции JavaScript пользуются [ключевыми словами](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function) `async` и `await`, которые позволяют обращаться с функциями JavaScript так, будто это были синхронные вызовы функций. ## Логика транзакции Теперь, когда вы ознакомились со структурированием и определением транзакций, сконцентрируемся на внутренней логике смарт-контракта. Вспомним первую транзакцию **issue** (выпустить): ``` 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**:
JavaScript ```JavaScript async issue(ctx, issuer, paperNumber, issueDateTime, maturityDateTime, faceValue) { // создать экземпляр ценной бумаги let paper = CommercialPaper.createInstance(issuer, paperNumber, issueDateTime, maturityDateTime, faceValue); // Смарт-контракт перемещает в состояние ISSUED (выпущена) paper.setIssued(); // Свежевыпущенная бумага пока еще принадлежит тому, кто ее выпустил paper.setOwner(issuer); // Внести эту ценную бумагу в список всех похожих бумаг в глобальное состояние в реестре await ctx.paperList.addPaper(paper); // Получить сериализованный вывод ценной бумаги и передать тому, кто вызвал смарт-контракт return paper.toBuffer(); } ```
Java ```Java @Transaction public CommercialPaper issue(CommercialPaperContext ctx, String issuer, String paperNumber, String issueDateTime, String maturityDateTime, int faceValue) { System.out.println(ctx); // создать экземпляр ценной бумаги CommercialPaper paper = CommercialPaper.createInstance(issuer, paperNumber, issueDateTime, maturityDateTime, faceValue,issuer,""); // Смарт-контракт перемещает в состояние ISSUED (выпущена) paper.setIssued(); // Свежевыпущенная бумага пока еще принадлежит тому, кто ее выпустил paper.setOwner(issuer); System.out.println(paper); // Внести эту ценную бумагу в список всех похожих бумаг в глобальное состояние в реестре ctx.paperList.addPaper(paper); // Получить сериализованный вывод ценной бумаги и передать тому, кто вызвал смарт-контракт return paper; } ```
Логика проста: на основании входных переменных транзакции создать новую коммерческую ценную бумагу `paper`, добавить ее в список всех коммерческих ценных бумаг при помощи `paperList`, и выдать коммерческую ценную бумагу (в сериализованном виде) как ответ транзакции. Посмотрите, как мы извлекаем `paperList` из транзакционного контекста, чтобы обеспечить доступ к списку коммерческих ценных бумаг. `issue()`, `buy()` and `redeem()` постоянно обращаются к `ctx.paperList`, чтобы поддерживать его в актуальном состоянии. Логика транзакции **buy** немного сложнее:
JavaScript ```JavaScript async buy(ctx, issuer, paperNumber, currentOwner, newOwner, price, purchaseDateTime) { // Извлечь текущую бумагу при помощи ключевых полей let paperKey = CommercialPaper.makeKey([issuer, paperNumber]); let paper = await ctx.paperList.getPaper(paperKey); // Проверить текущего владельца if (paper.getOwner() !== currentOwner) { throw new Error('Paper ' + issuer + paperNumber + ' is not owned by ' + currentOwner); } // Сначала транзакция изменяет состояние с ISSUED на TRADING if (paper.isIssued()) { paper.setTrading(); } // Проверка - не погашена ли бумага до сих пор (не находится ли в состоянии REDEEMED) if (paper.isTrading()) { paper.setOwner(newOwner); } else { throw new Error('Paper ' + issuer + paperNumber + ' is not trading. Current state = ' +paper.getCurrentState()); } // Изменить состояние бумаги await ctx.paperList.updatePaper(paper); return paper.toBuffer(); } ```
Java ```Java @Transaction public CommercialPaper buy(CommercialPaperContext ctx, String issuer, String paperNumber, String currentOwner, String newOwner, int price, String purchaseDateTime) { // Извлечь текущую бумагу при помощи ключевых полей String paperKey = State.makeKey(new String[] { paperNumber }); CommercialPaper paper = ctx.paperList.getPaper(paperKey); // Проверить текущего владельца if (!paper.getOwner().equals(currentOwner)) { throw new RuntimeException("Paper " + issuer + paperNumber + " is not owned by " + currentOwner); } // Сначала транзакция изменяет состояние с ISSUED на TRADING if (paper.isIssued()) { paper.setTrading(); } // Проверка - не погашена ли бумага до сих пор (не находится ли в состоянии REDEEMED) if (paper.isTrading()) { paper.setOwner(newOwner); } else { throw new RuntimeException( "Paper " + issuer + paperNumber + " is not trading. Current state = " + paper.getState()); } // Изменить состояние бумаги ctx.paperList.updatePaper(paper); return paper; } ```
Транзакция сначала проверяет имя текущего владельца `currentOwner` и то, что бумага `paper` находится в состоянии `TRADING` (торгуется), и только после этого меняет владельца `paper.setOwner(newOwner)`. Последовательность простейшая - сначала проверить какие-то обязательные пред-условия, затем установить в состоянии имя нового владельца, изменить состояние бумаги в реестре, а потом предоставить ответ транзакции в виде измененного статуса ценной бумаги (сериализованного в буфер). Теперь в качестве упражнения можете посмотреть, как работает транзакция **redeem** (погасить). ## Представление объекта Теперь, когда мы знаем, как задать и реализовать транзакции **issue**, **buy** и **redeem** при помощи классов`CommercialPaper` и `PaperList`, завершим эту главу изложением того, как работают классы. Найдите класс `CommercialPaper`:
JavaScript In the [paper.js file](https://github.com/hyperledger/fabric-samples/blob/{BRANCH}/commercial-paper/organization/magnetocorp/contract/lib/paper.js): ```JavaScript class CommercialPaper extends State {...} ```
Java In the [CommercialPaper.java file](https://github.com/hyperledger/fabric-samples/blob/release-1.4/commercial-paper/organization/magnetocorp/contract-java/src/main/java/org/example/CommercialPaper.java): ```Java @DataType() public class CommercialPaper extends State {...} ```
Этот класс содержит представление того, как хранится в памяти состояние ценной бумаги. Метод `createInstance` инициализирует коммерческую ценную бумагу полученными параметрами вот таким образом:
JavaScript ```JavaScript static createInstance(issuer, paperNumber, issueDateTime, maturityDateTime, faceValue) { return new CommercialPaper({ issuer, paperNumber, issueDateTime, maturityDateTime, faceValue }); } ```
Java ```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); } ```
Вспомним, как этот класс использовался транзакцией **issue**:
JavaScript ```JavaScript let paper = CommercialPaper.createInstance(issuer, paperNumber, issueDateTime, maturityDateTime, faceValue); ```
Java ```Java CommercialPaper paper = CommercialPaper.createInstance(issuer, paperNumber, issueDateTime, maturityDateTime, faceValue,issuer,""); ```
Примечательно, что каждый раз при вызове транзакции создания бумаги, создается новая копия ценной бумаги в памяти, и эта копия содержит данные транзакции. Важно отметить: * Это представление в памяти, а не в реестре; как оно появится в реестре, мы увидим [позже](#accessing-the-ledger). * Класс `CommercialPaper` наследует класс `State`. `State` - это класс, задающийся приложением, и он создает общую абстракцию состояния. Каждое состояние содержит класс бизнес-объекта, который оно представляет, составной ключ, может быть сериализовано или десериализовано, и так далее. Наличие `State` делает код более читаемым в случае, когда приходится хранить более одного типа бизнес-объектов в реестре. Посмотрите на класс `State` в [файле](https://github.com/hyperledger/fabric-samples/blob/{BRANCH}/commercial-paper/organization/magnetocorp/contract/ledger-api/state.js) `state.js`. * Ценная бумага вычисляет собственный ключ в момент собственного создания -- этот ключ будет использоваться при доступе к реестру. Этот ключ состоит из комбинации `issuer` и `paperNumber`. ```JavaScript constructor(obj) { super(CommercialPaper.getClass(), [obj.issuer, obj.paperNumber]); Object.assign(this, obj); } ``` * Транзакция, а не класс бумаги, перемещает бумагу в состояние `ISSUED` (выпущена). Причина этого в том, что жизненным циклом ценной бумаги управляет смарт-контракт. К примеру, транзакция `import` может создать ряд ценных бумаг, и сразу же в состоянии `TRADING` (торгуется). В остальном в классе `CommercialPaper` содержатся простые вспомогательные методы: ```JavaScript getOwner() { return this.owner; } ``` Припомните, что похожими методами пользуется смарт-контракт для проведения ценной бумаги по ее жизненному циклу. Например, как видели, транзакция **redeem** производит следующее: ```JavaScript if (paper.getOwner() === redeemingOwner) { paper.setOwner(paper.getIssuer()); paper.setRedeemed(); } ``` ## Доступ к реестру Найдите класс `PaperList` в `paperlist.js` [file](https://github.com/hyperledger/fabric-samples/blob/{BRANCH}/commercial-paper/organization/magnetocorp/contract/lib/paperlist.js): ```JavaScript class PaperList extends StateList { ``` Этот вспомогательный класс нужен, чтобы управлять всеми ценными бумагами сети PaperNet в базе данных состояний Hyperledger Fabric. Подробнее о структурах данных PaperList мы рассказываем в [главе об архитектуре](./architecture.html). Как и класс `CommercialPaper`, этот класс дополняет определяемый приложением класс `StateList`, который в свою очередь создает общую абстракцию для списка классов -- в нашем случае, для всех коммерческих ценных бумаг сети PaperNet. Этот метод `addPaper()` всего лишь косметическое прикрытие метода `StateList.addState()`: ```JavaScript async addPaper(paper) { return this.addState(paper); } ``` В [файле](https://github.com/hyperledger/fabric-samples/blob/{BRANCH}/commercial-paper/organization/magnetocorp/contract/ledger-api/statelist.js) `StateList.js` видно, как класс `StateList` пользуется Fabric API `putState()`, чтобы вписать ценную бумагу как данные состояния в реестр: ```JavaScript 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); } ``` Любая часть данных состояния в реестре обязана содержать два основных элемента: * **Ключ**: `key` формируется `createCompositeKey()`, используя постоянное название и ключ состояния `state`. Это название было присвоено при создании объекта `PaperList`, и посредством `state.getSplitKey()` определяется уникальный ключ каждого состояния. * **Данные**: `data` это просто состояние ценной бумаги в сериализованной форме, которая была создана методом `State.serialize()`. Класс `State`при помощи JSON сериализует и десериализует не только данные, но и класс бизнес-объекта состояния по необходимости, в нашем случае это класс `CommercialPaper`, который был задан при создании объекта `PaperList`. Примечательно, что `StateList` не хранит ничего, указывающего на индивидуальное состояние или полный список состояний -- всё это он делегирует базе данных состояний Fabric. Это важный шаблон проектирования -- тем самым снижается вероятность [конфликта версий реестра](../readwrite.html) в Hyperledger Fabric. Принадлежащие StateList методы `getState()` и `updateState()` работают сходным образом: ```JavaScript 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; } ``` ```JavaScript 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); } ``` Посмотрите, как они используют интерфейсы API Fabric `putState()`, `getState()` и `createCompositeKey()` чтобы обращаться к реестру. Мы распространим этот смарт-контракт позже на весь список ценных бумаг сети PaperNet -- постарайтесь представить, как в этом случае должен выглядеть метод, чтобы обращаться к реестру для этой цели? На этом всё! В этой главе вам удалось разобраться, как реализовывать смарт-контракты для PaperNet. Теперь можно переходить к следующему подразделу, описывающему в свою очередь, как приложение вызывает смарт-контракт при помощи Fabric SDK.