Migrating from Hyperledger Composer to Convector Framework — Marbles Example

A few weeks ago, Simon Stone, Lead Engineer at the IBM Blockchain team, announced that IBM will significantly reduce its efforts in the Hyperledger Composer project, leaving a lot of developers with existing applications in a cloudy situation wondering what to do next.

The good news, here at WorldSibu we have been working in an Smart Contract framework that runs natively in Hyperledger Fabric, so you don’t have to think about managing the complexities of communicating with Fabric directly, but still running a native Node JS chaincode on it. Additionally on top of it, we’re building several tools to make the developer’s life easier, like a dev environment ready to be used out of the box, just like you used to work in Composer.

Right now Convector supports Hyperledger Fabric already, and we’re planning to add support for multiple Blockchain techs soon, stay tuned!

In this example, I’ll show you how you can easily migrate from Composer to Convector. This can be done without too much efforts since we share pretty similar concepts.

The first difference between the two is the language. Composer has decided to go with its custom designed language to define the different pieces of a smart contract. Convector on the other hand is crafted using Typescript, a super set language based on Javascript that has been gaining traction because of all the rich features it has, like optional strong typing and abstract syntax trees (AST) generation to create tools around the code.

In Composer, a Business Network model is defined by essentially three components:

  • Assets: houses and listings
  • Participants: buyers and homeowners
  • Transactions: buying or selling houses, and creating and closing listings

When we were designing Convector we stoped here and thought: assets and participants are basically models compound by different properties, and even more, participants can sometimes act as the one making an action, or the one the action is being made on!

We wanted to keep things as simple as possible, and give the developers the ability to express their ideas with a more software development friendly terminology, thus we decided to create two fundamental concepts inside the framework:

  • Models: A reference to something in the real world, tangible or intangible, describing its properties. It might as well be a participant in the network.
  • Controllers: A set of rules on how you can interact with the models.

Based on those two fundamental concepts you can build everything you need for your chaincode and yet have enough flexibility to express your logic the way you need.

The following piece of code defines the Marbles example in Composer.

namespace org.hyperledger_composer.marbles
enum MarbleColor {
o RED
o GREEN
o BLUE
o PURPLE
o ORANGE
}
enum MarbleSize {
o SMALL
o MEDIUM
o LARGE
}
asset Marble identified by marbleId {
o String marbleId
o MarbleSize size
o MarbleColor color
--> Player owner
}
participant Player identified by email {
o String email
o String firstName
o String lastName
}

In Convector you write it this way:

export enum MarbleColor {
RED,
GREEN,
BLUE,
PURPLE,
ORANGE
}
export enum MarbleSize {
SMALL,
MEDIUM,
LARGE
}
export class Marble extends ConvectorModel<Marble> {
@ReadOnly()
@Required()
public type = 'io.worldsibu.marbles.marble';
@Validate(yup.number())
@Default(MarbleSize.MEDIUM)
public size: MarbleSize;
@Validate(yup.number())
public color: MarbleColor;
@Validate(yup.string().email())
public owner: string;
}
export class Player extends ConvectorModel<Player> {
@Required()
@ReadOnly()
public type = 'io.worldsibu.marbles.player';
@Validate(yup.string().email())
public id: string;
@Validate(yup.string())
public firstName: string;
@Validate(yup.string())
public lastName: string;
}

Since Convector runs on Javascript, you can have expressions for validations and restrict the data your model contains in a more expressive way. We have created a set of useful decorators to help you achieve this and save you some code while keeping the models readable (and reducing risks!). There are two properties a model must have, a type which is a unique identifier for this model class and the id which is an unique identifier for each model instance. If you don’t specify and id field, Convector will declare a string field for you.

In Composer the chaincode logic, a.k.a the rules that defines what you can do with the data, resides in 3 different layers: ACL, Transaction Models, and Transaction Functions.

First, you start by defining the access permissions in the ACL (access control list):

rule Default {
description: "Allow all participants access to all resources"
participant: "org.hyperledger_composer.marbles.player"
operation: ALL
resource: "org.hyperledger_composer.marbles.*"
action: ALLOW
}

Then you define the transaction model:

transaction TradeMarble {
--> Marble marble
--> Player newOwner
}

And finally you define the transaction function:

async function tradeMarble(tradeMarble) {
tradeMarble.marble.owner = tradeMarble.newOwner;
const assetRegistry = await getAssetRegistry('org.hyperledger_composer.marbles.Marble');
await assetRegistry.update(tradeMarble.marble);
}

In Convector we simplified all this logic using Controllers

@Controller('marble')
export class MarbleController extends ConvectorController {

@Invokable()
public async create(@Param(Marble) marble: Marble) {
await marble.save();
}
@Invokable()
public async trade(
@Param(yup.string()) marbleId: string,
@Param(yup.string().email()) newOwner: string
): Promise<void> {
// use this.sender for authorization checks
const marble = await Marble.getOne(marbleId);
marble.owner = newOwner;
await marble.save();
}
}

You can define the transaction model using the @Invokable and @Param decorators. Any Invokable method will be exposed as a transaction, and the Params describe its signature. You're not restricted to primitives only, you can pass Models as well, Convector will parse all complex objects for you.

You can define the access rights using this.sender, which is the sender’s fingerprint, unique per participant. You can do all sort of things with it, even attach multiple senders to a single participant if you want. This is the flexibility it provides so you can define your own rules.

And finally, you write all the necessary extra logic for the transaction function. You can do almost all kind of operations in here, as long as they are deterministic and use no external data other than the ledger data. For querying the ledger, every model has some useful CRUD methods like getOne or getAll, save, update and delete. There are some others like history, which retrieves the history of updates for that model, clone to duplicate an asset or assign to batch assign properties to the model.

If you want to test the above methods in Composer, this is what you usually do:

const namespace = 'org.hyperledger_composer.marbles';
const factory = businessNetworkConnection.getBusinessNetwork().getFactory();
const dan = factory.newResource(namespace, 'Player', 'daniel.selman@example.com');
dan.firstName = 'Dan';
dan.lastName = 'Selman';
const simon = factory.newResource(namespace, 'Player', 'sstone1@example.com');
simon.firstName = 'Simon';
simon.lastName = 'Stone';
const playerRegistry = await businessNetworkConnection.getParticipantRegistry(namespace + '.Player');
await playerRegistry.addAll([dan, simon]);
const marble = factory.newResource(namespace, 'Marble', 'MARBLE_001');
marble.size = 'SMALL';
marble.color = 'RED';
marble.owner = factory.newRelationship(namespace, 'Player', dan.$identifier);
const marbleRegistry = await businessNetworkConnection.getAssetRegistry(namespace + '.Marble');
await marbleRegistry.add(marble);
const tradeMarble = factory.newTransaction(namespace, 'TradeMarble');
tradeMarble.newOwner = factory.newRelationship(namespace, 'Player', simon.$identifier);
tradeMarble.marble = factory.newRelationship(namespace, 'Marble', marble.$identifier);
await businessNetworkConnection.submitTransaction(tradeMarble);

In Convector, we have adapters to be able to use the same controller and model in the client applications.

const adapter = new FabricControllerAdapter(configuration);
const playerCtrl = new PlayerControllerClient(adapter);
const marbleCtrl = new MarbleControllerClient(adapter);
const dan = new Player({
id: 'daniel.selman@example.com',
firstName: 'Dan',
lastName: 'Selman'
});
await playerCtrl.register(dan);
const simon = new Player({
id: 'sstone1@example.com',
firstName: 'Simon',
lastName: 'Stone'
});
await playerCtrl.register(simon);
const marble = new Marble({
id: '1',
size: MarbleSize.SMALL,
color: MarbleColor.RED,
owner: dan.id
});
await marbleCtrl.create(marble);
await marbleCtrl.trade(marble.id, simon.id);

Each adapter has its own internal logic, the FabricControllerAdapter for instance, handles the network profile for communication with Fabric, but Convector is not tightly coupled with Fabric, so the community can create different adapters based on their needs. This is exactly how we plan to support other blockchains in the near future, by replacing the adapter with a new implementation.
Adapters don’t have any special treatment inside convector, they just dictate the communication with the blockchain layer so that the standard adapters that we have in Convector can be replaced by a custom implementation provided by the developers or the community.

Composer and Convector share a lot of similarities, yet they took different architectural paths. With the Convector controllers you pretty much are in control of all the logic inside the chaincode and have much more flexibility to design architectural decision you consider are appropriate for your use case.

We’re constantly adding new features to the framework and we’ll love to hear from you about new features that might be useful for the community. You can create new issues or even drop a PR in our github at any time.


Originally published at hackernoon.com on September 21, 2018.