Blockchain And JavaScript: How To Offer Your Smart Contracts As A Service

0
113

Recently, I started to work with a large team of frontend and backend developers. The project was easy, The only smart contract I developed needed a few hours to be ready to use. But I had to face a serious challenge as the majority of my team has zero background about blockchain. To make the situation more complicated it was required to never use Metamask or any wallet as the expected users of the project aren’t familiar with the blockchain. To solve this problem, Our team had created a javascript class to interact with any smart contract deployed on our private network as Blockchain service.

What Exactly Is A Blockchain Service Class

As a developer with zero blockchain background, the easier solution we have is to create a class that will interact with the blockchain. The services class will be used by frontend and backend developers. Each developer could use this class in a different way depending on his need. The class will be something like that

For this article, I created the class to be as Flexible as much as I can. The blockchain service class will be simple for the backend but the frontend, it will be a bit harder. Let’s start with the easy one.

For Backend Developers

Most of the backend developer will need the Blockchain class for only two things first to read data from the blockchain and parse it to the frontend or the most important to make admin tasks like adding addresses or give permissions.

For example, We may need to submit transactions as the owner of the smart contract, give permission or send assets to certain addresses. Only admin could do that.

For Frontend Developers

The most important task is to initialize the user account. Remember as blockchain developer you need to let users make transactions on their own. But how are you going to achieve that if you don’t use any wallet? To achieve that we need to initialize a user class. Sometime we will need his private key to submit a transaction or to check privilege. Other users need only to read some data public data so we don’t need their private key in this case. We are sure on any scenario user must keep his address for himself and don’t send it to our service.

Now how we are going to do that?

Let’s Start Coding

The most important step is to find a way to sign transactions without using Metamask then we will need to find a way to generate private keys for users and keep the private keys safe. The last step is to offer the frontend developer a flexible way to manage user accounts or to submit transaction only. Let’s go.

Metamask/Wallets Alternatives

The best alternative I found was to use a wallet provider. The same npm package module we use to migrate contracts on Truffle.

npm install @truffle/hdwallet-provider

How does it work? We will need the private key only to sign the transactions then we will use path and fs npm packages to load the build file of the contract (the contract artificats)like that:

const contract = require('@truffle/contract');
const WalletProvider = require('@truffle/hdwallet-provider');
// assume for now you already have the contract artificaits
const contractArtificaits;
const contract = contract(contractArtificaits);
const instance = await this.contract.deployed();
// instance could be used to interact with the smart contract

To use const instance you need to write something like that

await this.instance[<the method name>](<method argument>,{from:<your account public address>});
/* 'from' keyword may not be useful to read data from blockchain. Unless you try to read from function that use modifier*/

Now, we are ready to create our class and interact with our contracts. before start creating our class we will have to add some npm modules. We will need path & fs to load build files of contract and web3 & hdWalletProvider to interact with solidity contract we will talk about that in details later. And last crypto-js to encrypt the private key. We will talk about that at the end of the article.

'use strict';
const contract = require('@truffle/contract');
const WalletProvider = require('@truffle/hdwallet-provider');
const CryptoJS = require("crypto-js");
const path= require('path');
const fs = require('fs');
const Web3 = require('web3');
const web3 = new Web3(new Web3.providers.HttpProvider('http://127.0.0.1:8545/'));
const web3Provider = new Web3.providers.HttpProvider('http://127.0.0.1:8545/');

Create a Class

We will name the class Contract then create a constructor with an optional private key as a parameter.

class Contract{
constructor(privateKey = ''){
this.privateKey = privateKey;
}

}

To initialize the instance for the interaction with our smart contract. We have two possibilities if we have the private key or we have not. If we already have the private key we will create the instance using wallet provider as above. Else we will use the default way using web3 . Web3js is one of the most famous libraries to interact with Solidity. The main difference in initialization between the two classes is the provider.

Create initContract() that takes the contract name then got the artificats for contract and check if the private key exists we initialize contract with HDWalletProvider to sign transaction else we use Web3 to read transaction.

/*Inside Contract Class*/
async initContract(contractName){
/*Get Contract Articicates*/
const filePath = path.resolve('build/contracts/'+contractName+'.json');
let rawdata = fs.readFileSync(filePath);
this.contractArtificaits = JSON.parse(rawdata);
this.contract = contract(this.contractArtificaits);
/*Check private key*/
if (this.privateKey) {
await this.WalletProviderInitContract();
}
else{
await this.web3ProviderInitContract();
}
this.instance = await this.contract.deployed();
return this.instance;
}
/*Create contract instance using WalletProvider*/
async WalletProviderInitContract(){
this.provider = new WalletProvider(this.privateKey,"http://127.0.0.1:8545/");
this.contract.setProvider(this.provider);
}
/*Create contract instance using Web3*/
async web3ProviderInitContract(){
this.contract.setProvider(web3Provider);
}

Now we can use this.instance to execute contract methods.

Create Getter and Setter Methods

initContract is a private function we can use it internally only when we need to call a function from any contract. For getter and setter methods, we are free to sign a transaction or read from our private key or if we need to use another one. (It’s optional, I add this feature to make my code as much flexible for my team)

/*Read Transactions*/
async getter(contractName,methodName,args,_privateKey ='') {
if (_privateKey !== '') {
this.privateKey = _privateKey;
}
await this.initContract(contractName)
if (this.privateKey) {
return await this.instance[methodName](...args,{from:web3.eth.accounts.privateKeyToAccount(this.privateKey).address});
}
else{
return await this.instance[methodName](...args);
}
}
/*Submit Transactions*/
async setter(contractName,methodName,args,_privateKey ='') {
if (_privateKey !== '') {
this.privateKey = _privateKey;
}
await this.initContract(contractName);
const value = await this.instance[methodName](...args,{from:web3.eth.accounts.privateKeyToAccount(this.privateKey).address});
return value;
}

The last thing to do is adding a static function to interact with contracts without initializing the Contract class object. This function is created for the backend if they need to make a transaction. And don’t need to use a class.

static async useContract(method,contractName,methodName,args,_privateKey ='') {
const contract = new Contract()
if (method === 'get') {
return await contract.getter(contractName,methodName,args,_privateKey);
}
else if(method === 'set'){
return await contract.setter(contractName,methodName,args,_privateKey);
}
else{
throw Error('Invalid Option');
}
}

After we are done we should have something like that.

'use strict';
const contract = require('@truffle/contract');
const WalletProvider = require('@truffle/hdwallet-provider');
const path= require('path');
const fs = require('fs');
const CryptoJS = require("crypto-js");
const Web3 = require('web3');
const web3 = new Web3(new Web3.providers.HttpProvider('http://127.0.0.1:8545/'));
const web3Provider = new Web3.providers.HttpProvider('http://127.0.0.1:8545/');
class Contract {

constructor(privateKey='') {
this.privateKey = privateKey;
}


/*Contract Initialization */
async initContract(contractName){
const filePath = path.resolve('build/contracts/'+contractName+'.json');
let rawdata = fs.readFileSync(filePath);
this.contractArtificaits = JSON.parse(rawdata);
this.contract = contract(this.contractArtificaits);
if (this.privateKey) {
await this.WalletProviderInitContract();
}
else{
await this.web3ProviderInitContract();
}
this.instance = await this.contract.deployed();
return this.instance;
}

async WalletProviderInitContract(){
this.provider = new WalletProvider(this.privateKey,"http://127.0.0.1:8545/");
this.contract.setProvider(this.provider);
}

async web3ProviderInitContract(){
this.contract.setProvider(web3Provider);
}

/*Account creation */
static async getAccount(privateKey) {
return web3.eth.accounts.privateKeyToAccount(privateKey);
}
static async createAccount() {
const newAccout = await web3.eth.accounts.create();
return newAccout;
}
/*Transactions submmitions */
async getter(contractName,methodName,args,_privateKey ='') {
if (_privateKey !== '') {
this.privateKey = _privateKey;
}
await this.initContract(contractName)
if (this.privateKey) {
return await this.instance[methodName](...args,{from:web3.eth.accounts.privateKeyToAccount(this.privateKey).address});
}
else{
return await this.instance[methodName](...args);
}
}
async setter(contractName,methodName,args,_privateKey ='') {
if (_privateKey !== '') {
this.privateKey = _privateKey;
}
await this.initContract(contractName);
const value = await this.instance[methodName](...args,{from:web3.eth.accounts.privateKeyToAccount(this.privateKey).address});
return value;
}

static async useContract(method,contractName,methodName,args,_privateKey ='') {
const contract = new Contract()
if (method === 'get') {
return await contract.getter(contractName,methodName,args,_privateKey);
}
else if(method === 'set'){
return await contract.setter(contractName,methodName,args,_privateKey);
}
else{
throw Error('Invalid Option');
}
}
}  
module.exports = Contract

This was the hard part next we need to generate private, the public key for users then found a way to submit keys on a secure way.

Create a Blockchain Account

Web3 offer an easy way to create blockchain account using create() like that

static async createAccount() {
const newAccount = await web3.eth.accounts.create();
return newAccount;
}

newAccount will return a JSON object with the address and the private key. Now user should keep his key safe to use his account. But what if we need to submit a transaction by him on the server and we don’t want to save his private key?

The best answer I found was using encrypt my the private key using user password the user has the password and I have the encrypted text only

/* Private Key Management */
static async createRandomEncryptedPrivateKey(password) {
const newAccount = web3.eth.accounts.create();
const privateKey = newAccount.privateKey;
const encryptedPrivateKey = CryptoJS.AES.encrypt(privateKey, password).toString();
return encryptedPrivateKey;
}
static async accountFromEncryptedPrivateKey(encryptedPrivateKey, password) {
const decryptedObject = CryptoJS.AES.decrypt(encryptedPrivateKey, password);
const decryptedPrivateKey = decryptedObject.toString(CryptoJS.enc.Utf8);
return web3.eth.accounts.privateKeyToAccount(decryptedPrivateKey);
}

Finally, our class should look like that

'use strict';
const contract = require('@truffle/contract');
const WalletProvider = require('@truffle/hdwallet-provider');
const path= require('path');
const fs = require('fs');
const CryptoJS = require("crypto-js");
const Web3 = require('web3');
const web3 = new Web3(new Web3.providers.HttpProvider('http://127.0.0.1:8545/'));
const web3Provider = new Web3.providers.HttpProvider('http://127.0.0.1:8545/');
class Contract {

constructor(privateKey='') {
this.privateKey = privateKey;
}
static async createAccount() {
const newAccount = await web3.eth.accounts.create();
return newAccount;
}

/*Contract Initialization */
async initContract(contractName){
const filePath = path.resolve('build/contracts/'+contractName+'.json');
let rawdata = fs.readFileSync(filePath);
this.contractArtificaits = JSON.parse(rawdata);
this.contract = contract(this.contractArtificaits);
if (this.privateKey) {
await this.WalletProviderInitContract();
}
else{
await this.web3ProviderInitContract();
}
this.instance = await this.contract.deployed();
return this.instance;
}

async WalletProviderInitContract(){
this.provider = new WalletProvider(this.privateKey,"http://127.0.0.1:8545/");
this.contract.setProvider(this.provider);
}

async web3ProviderInitContract(){
this.contract.setProvider(web3Provider);
}

/*Account creation */
static async getAccount(privateKey) {
return web3.eth.accounts.privateKeyToAccount(privateKey);
}
static async createAccount() {
const newAccout = await web3.eth.accounts.create();
return newAccout;
}
/*Transactions submmitions */
async getter(contractName,methodName,args,_privateKey ='') {
if (_privateKey !== '') {
this.privateKey = _privateKey;
}
await this.initContract(contractName)
if (this.privateKey) {
return await this.instance[methodName](...args,{from:web3.eth.accounts.privateKeyToAccount(this.privateKey).address});
}
else{
return await this.instance[methodName](...args);
}
}
async setter(contractName,methodName,args,_privateKey ='') {
if (_privateKey !== '') {
this.privateKey = _privateKey;
}
await this.initContract(contractName);
const value = await this.instance[methodName](...args,{from:web3.eth.accounts.privateKeyToAccount(this.privateKey).address});
return value;
}

static async useContract(method,contractName,methodName,args,_privateKey ='') {
const contract = new Contract()
if (method === 'get') {
return await contract.getter(contractName,methodName,args,_privateKey);
}
else if(method === 'set'){
return await contract.setter(contractName,methodName,args,_privateKey);
}
else{
throw Error('Invalid Option');
}
}
/* Private Key Management */
static async createRandomEncryptedPrivateKey(password) {
const newAccount = web3.eth.accounts.create();
const privateKey = newAccount.privateKey;
const encryptedPrivateKey = CryptoJS.AES.encrypt(privateKey, password).toString();
return encryptedPrivateKey;
}
static async accountFromEncryptedPrivateKey(encryptedPrivateKey, password) {
const decryptedObject = CryptoJS.AES.decrypt(encryptedPrivateKey, password);
const decryptedPrivateKey = decryptedObject.toString(CryptoJS.enc.Utf8);
return web3.eth.accounts.privateKeyToAccount(decryptedPrivateKey);
}
}  
module.exports = Contract

For now, our team is looking for the best way to offer blockchain as a service to implement blockchain in a faster and easier way. Then we will create our microservice. If you have any better solution, please share it with me. I hope that I was clear. However, if you have any question, please don’t hesitate to ask me anytime.

We are planning to implement more features like getting events logs and to separate blockchain service for frontend team then the blockchain service for backend team. And we are happy to hear your opinion or any suggestion to make a better solution.


Blockchain And JavaScript: How To Offer Your Smart Contracts As A Service was originally published in Coinmonks on Medium, where people are continuing the conversation by highlighting and responding to this story.