Writing Smart Contracts using Convector — a Fullstack Typescript Framework

Writing smart contracts for any blockchain shouldn't be a hard thing to do. A blockchain developer is nothing more than a software developer, that knows a couple of special rules that apply in its code (checkout our quick intro to blockchain development for software developers). The biggest problem right now is having to learn a whole new programming language just to do one job. This slows down the adoption of the technology and thus the innovation of new developers entering the field.

That's why we created Convector, a Typescript (Javascript) framework for blockchain development systems. It's based on the MVC pattern, more specifically in the Model and Controller side.

We write all our code in Typescript, because it has proved us the flexibility and security of maintaining a big codebase and the utility to have models shared between the whole stack, between chaincode and server side to the browser, all in a single typed language. Since typescript compiles down to javascript, the packages can be imported in vanilla javascript projects, but we make a strong use of decorators and using them in vanilla might be complicated.

As a blockchain developer your main responsibility is to write and think about two things: What data do I want to store? What rules does this data need?

When you know what you want to store, the best idea is to start modeling the data, just like you do when programing a database. In Convector, writing a model is as easy as extending the ConvectorModel class.

class Product extends ConvectorModel<Product> {
@ReadOnly()
@Required()
type = 'io.worldsibu.example.product';
  @Required()
@Validate(yup.string())
name: string;
  @Required()
@Validate(yup.number())
price: number;
  @Required()
@Validate(yup.string())
owner: string;
  @ReadOnly()
@Default(() => Date.now())
@Validate(yup.number())
created: number;
}

Even if you're not familiar with Typescript, it's pretty obvious what's going in here: you have a model called Product with a set of properties, each of them has some flags describing the rules the data has to follow. For example, ReadOnly is a modifier to not allow modifications after being set, Required makes sure the data complies with a certain schema validation (we support yup for now), Default/Required ensure at write time, that this property will have a value, either by providing one in code or by expecting the user to set one.

ConvectorModel provides an id field, all the models have one, and it's the key being used to identify the instance of this model in the blockchain.

Convector is made in a modular way, we want to support not only Hyperledger Fabric, but all the possible blockchains out there! And models are not used in the chaincode codebase only, you'll probably want to use the same structure in your Nodejs server, your angular website or your react native application. For this reason the models are separated from the actual implementation layer that talks with the underlying storage layer. For write operations right now we have the FabricStorage and for querying data we have the CouchDBStorage. We have some more planned in the upcoming months, but the community is free to develop their own storage implementations.


Next thing you'll need is the set of rules and policies for your data. This is where the ConvectorController is useful.

@Controller('product')
class ProductCtrl extends ConvectorController {
  @Invokable() 
async create(
@Param(Product) product: Product
) {
const _product = await Product.getOne(id);
    if (product.id === _product.id) {
throw new Error('Product ID already created');
}
    product.owner = this.sender;
    await product.save();
}
  @Invokable()
async transfer(
@Param(yup.string()) id: string,
@Param(yup.string()) to: string
) {
const product = await Product.getOne(id);
    if (product.owner !== this.sender) {
throw new Error('Only the owner can transfer the product');
}
    product.owner = to;
    await product.save();
}
}

Here we have a controller to manipulate our products in the blockchain. A single chaincode can be made of multiple controllers, so we need to register this controller using a name. In this case product is our namespace.

Controllers are installed in a chaincode using NPM, so they must be valid node modules with a package.json file.

This Product controller is made up of two methods, one to create a new product and another one to transfer its ownership. Not all the methods in your controllers are invokable from the outside world, you can expose the ones you want by using Invokable and keep the rest private.

Convector offers a parameter validation using yup. We also support models as parameters and we cast the object for you.

In a controller method, throwing an error is the way to reject a transaction. If the sender doesn't not have the permissions to invoke the function, you simply throw and no changes will be committed.

Controllers, in the same way as models, are used outside the blockchain too. You want to be able to invoke methods from your nodejs or front-end application and you don't want to start creating custom wrappers of logic around something you already built and replicate the same functions with the same parameters everywhere! We got your back here. Convector is a full-stack framework and we auto-generate a client interface of your controller for you, so it can be used with any ConvectorAdapter.

Adapters follow the same principle as the Model Storage. We have a FabricAdapter capable to talk with the Fabric SDK and send transactions to multiple peers. We also offer a MockAdapter, useful for unit-tests and mock the blockchain behavior. We're planning more adapters to come, like services for Angular or components for React. If you're interested in a specific adapter, left your suggestions in our github issues.


Writing a smart contract is a really simple task, but the logic inside has to be verified and confirmed to be working. Unit tests are a essential feature of a blockchain framework and Convector is no different.

describe('ProductController' () => {
let adapter: MockControllerAdapter;
let productCtrl: ProductCtrl;
  beforeEach(async () => {
adapter = new MockControllerAdapter();
productCtrl = new ProductCtrl(adapter);
    await adapter.init([
{
version: '*',
controller: 'ProductCtrl',
name: '/path/to/controller-package'
}
]);
});
  it('should create a product', async () => {
const productId = uuid();
    await productCtrl.create(new Product({
id: productId,
name: 'OnePlus 6',
price 600
}));
    const product = await adapter.getById<Product>(productId);
    expect(product).to.exist;
expect(product.name).to.eq('OnePlus 6');
});
});

To execute the unit tests you don't need a full blockchain running. Since the logic inside controller has to be deterministic, unit testing the functions can be done using regular javascript testing tools like Mocha or Jasmine.

Finally, we offer some tools to make local development really easy. The dev-env contains two organizations with one peer each and one orderer. If you're not familiar with the Hyperledger Fabric concepts, this is pretty much all the infrastructure necessary to run a blockchain, but thanks to Convector you dodn't have to learn how to configure all that just to make a hello world.

Alongside with the dev-env, the chaincode manager is really helpful to package, install, update and invoke the chaincode in the dev-env, so you can now just focus on writing your amazing new project's code.


Conclusion

Writing smart contracts doesn't have to be a hard task or demand hours of reading technical books with complex math just to store some data in a blockchain. Convector helps you eliminating the distractions and put you on track to just write code.

If you're still curious about Convector, checkout our guide on how to write a full stack application, or checkout our repo, we're an open source project in case you want to contribute moving the blockchain development forward.