Certifications on the Blockchain

Download Source Code.

Certifications on the Blockchain

This project is designed to let you issue out non-transferable certifications or assets on the NEM blockchain. The idea is a student might take an exam, and then you issue a certificate and place that even/cert on the blockchain so potential employers can verify.

This can be used for any type of non-transferable smart asset.

This particular code example also transfers a cryptocurrency reward to the user along with the certification

Pre-requisites

Here are some pre-requisite steps that must happen before you can use this code:

  1. Create a Mosaic called non-transferable or something similar. This will be used as a Levy for your certificates so people cannot transfer it to anyone else
  2. Create a Mosaic for each certificate you want to issue

    • Make it mutable
    • Make initial quantity 1
    • Don’t add divisibility
    • Make it transferable
    • Set a levy to your non-transferable mosaic and set Absolute to 1
    • Give it a name and description/etc
  3. You will need to export the wallet file for the account that owns these mosaics. Then the wallet would be uploaded to your server so it can sign transactions
  4. Create a nem-config.json file and set the data appropriately, then also upload this to your server

Example certification creation

Certification Example

The Code

The entire logic is in one file: index.ts:

import {
    AccountHttp,
    Address,
    MosaicSupplyChangeTransaction,
    TimeWindow,
    MosaicId,
    Account,
    NetworkTypes,
    MosaicSupplyType,
    SimpleWallet,
    Password,
    TransactionHttp,
    NEMLibrary,
    TransferTransaction,
    MosaicHttp,
    EmptyMessage,
    NemAnnounceResult
} from 'nem-library';
const fs = require('fs');
import { Observable } from 'rxjs/Observable';
const nemConfig = require('./nem-config.json');

NEMLibrary.bootstrap(NetworkTypes.TEST_NET);
const transactionHttp = new TransactionHttp();
const mosaicHttp = new MosaicHttp();

export interface CertificationGroup {
    namespace: string;
    mosaicName: string;
    reward: number;
}

export interface CertificationTransfer {
    toAddress: Address;
    signer: Account;
    rewardAmount: number;
    certMosaicId: MosaicId;
    rewardMosaicId: MosaicId;
}

/**
 * 1. Check if the address already owns this certification
 * 2. If not, generate 1 new certification mosaic
 * 3. Then transfer certification mosaic and reward to address
 * @param {string} address Address of the incoming request
 * @param {string} certName Name of the certification earned
 */
export const processCertification = async (address: string, certName: string) => {
    try {
        // Grab data from config json file
        const cert: CertificationGroup = nemConfig.certifications[certName];
        if (!cert) throw Error('Invalid certification name');
        const mosaicId = new MosaicId(cert.namespace, cert.mosaicName);

        // Only proceed if cert not already owned
        const isOwned = await certificationOwned(address, cert.mosaicName);
        if (isOwned) throw Error('This certification is already owned by address');

        // Create 1 new certification asset
        const certCreated = await createCertifcationMosaic(mosaicId);
        if (!certCreated) return;

        // Transfer certification and reward to address
        const transfer: CertificationTransfer = {
            toAddress: new Address(address),
            signer: loadAccount(),
            rewardAmount: cert.reward,
            certMosaicId: mosaicId,
            rewardMosaicId: new MosaicId(nemConfig.mosaicNamespace, nemConfig.mosaicForReward)
        };
        await transferMosaics(transfer);

    } catch (err) {
        console.log(err);
    }
};

/**
 * Fetch mosaic balances for given address
 * Iterate through mosaics to see if certification is owned
 * @param {string} address Address to search balances on
 * @param {string} certName Certification to check existence of
 * @returns {Promise<boolean>}
 */
export const certificationOwned = (address: string, certName: string): Promise<boolean> => {
    return new Promise<boolean>((resolve, reject) => {
        const accountHttp = new AccountHttp();
        try {
            const add = new Address(address);
            accountHttp.getMosaicOwnedByAddress(add)
                .subscribe(mosaics => {
                    const cert = mosaics.find((mosaic) => {
                        return mosaic.mosaicId.name === certName
                    });
                    if (!cert) resolve(false);
                    else { resolve (true); }
                }, err => {
                    reject(err);
                });
        } catch (err) {
            reject(err);
        }
    });
};

/**
 * Load the Account through the wallet file and password
 * @returns {Account}
 */
const loadAccount = (): Account => {
    const contents = fs.readFileSync(nemConfig.walletPath);
    const wallet = SimpleWallet.readFromNanoWalletWLF(contents);
    const pass = new Password(nemConfig.walletPassword);
    return wallet.open(pass);
};

/**
 * Create 1 new certification to be immediately transferred
 * @param {MosaicId} mosaic
 * @returns {Promise<boolean>}
 */
export const createCertifcationMosaic = (mosaic: MosaicId) => {
    return new Promise<boolean>((resolve, reject) => {
        const supplyChangeTransaction = MosaicSupplyChangeTransaction
            .create(TimeWindow.createWithDeadline(), mosaic, MosaicSupplyType.Increase, 1);
        const account = loadAccount();
        const signed = account.signTransaction(supplyChangeTransaction);
        transactionHttp.announceTransaction(signed)
            .subscribe(_ => {
                resolve(true);
            }, err => {
                reject(err);
            });
    });
};

export const transferMosaics = (certTransfer: CertificationTransfer): Promise<NemAnnounceResult> => {
    return new Promise<NemAnnounceResult>((resolve, reject) => {
        const transactionHttp = new TransactionHttp();
        Observable.from([
            mosaicHttp.getMosaicTransferableWithAmount(certTransfer.certMosaicId, 1),
            mosaicHttp.getMosaicTransferableWithAmount(certTransfer.rewardMosaicId, certTransfer.rewardAmount / 1e6)])
            .flatMap(transfer => transfer)
            .toArray()
            .map(mosaics => TransferTransaction.createWithMosaics(
                TimeWindow.createWithDeadline(),
                certTransfer.toAddress,
                mosaics,
                EmptyMessage))
            .map(transaction => certTransfer.signer.signTransaction(transaction))
            .flatMap(signed => transactionHttp.announceTransaction(signed))
            .subscribe(result => {
                resolve(result);
            }, error => {
                reject(error);
            });
    });
};