Source: templates/substratebasedcrypto.js

const AbstractCryptoModule = require('./abstractcryptomodule')
const { Keyring, decodeAddress, encodeAddress, createPair } = require('@polkadot/keyring');
const { ApiPromise, WsProvider } = require('@polkadot/api');
const { randomBytes } = require('crypto');

/**
* An Implementation of AbstractCryptoModule that supports any substrate-based module
* @extends AbstractCryptoModule
* @example
* class Polkadot extends SubstrateBasedCrypto {
*   constructor(app) {
*     super(app, 'DOT', saitoEndpoint, "Polkadot's Existential Deposit is 1 DOT, be sure not to send less than 1 DOT or leave less than 1 DOT in your wallet.");
*     this.name = 'Polkadot';
*     this.description = 'Polkadot application layer for in-browser Polkadot applications. Install this module to make Polkadot your default in-browser cryptocurrency';
*     this.categories = "Cryptocurrency";
*   }
* ...
*/
class SubstrateBasedCrypto extends AbstractCryptoModule {

  /**
   * Construct SubstrateBasedCrypto
   * @param {Object} app - Saito Application Context
   * @param {String} ticker - Ticker symbol of underlying Cryptocurrency
   * @param {String} endpoint - URI of Substrate Endpoint location
   * @param {String} info - Option info about this module
   * @example 
   * super(app, "DOT", localhost:3838, 'DOT mainnet')
   */
  constructor(app, ticker, endpoint, info = '') {
    super(app, ticker);
    this.endpoint = endpoint;
    //this.subscanEndpoint = "https://westend.api.subscan.io/";
    this.subscanEndpoint = "https://parity.saito.io:9931/subscanapi";
    //this.subscanEndpoint = "http://kaolinite/subscanapi";
    this.info = info;
    this._api = null; // treat as private, please use getApi to access
    this.mods = [];
    this.keypair = null;
    this.keyring = null;
  }

 /**
  * installModule runs when a node initialized a module for the first time
  * @param {Object} app - Saito Application Context
  */
  installModule(app) {
    // if a module would like to forcibly set itself as the preffered crypto
    // this should be done:
    // app.wallet.setPreferredCrypto(this.ticker);
  }
 /**
  * Initialize Endpoint API connection and keyring
  * @param {Object} app - Saito Application Context
  */
  initialize(app) {
    if (app.BROWSER) {
      // load optionStorage from local storage
      this.load();
      this._api = null;
      let that = this;
      this.eventEmitter.on('activated', () => {
        const wsProvider = new WsProvider(this.endpoint);
        that._api = new ApiPromise({ provider: wsProvider });
        that._api.on('connected', (stream) => {
          //console.log(this.description + ' Polkadot Socket Provider connected');
        });
        that._api.on('disconnected', (stream) => {
          //console.log(this.description + ' Polkadot Socket Provider disconnected');
        });
        that._api.on('ready', (stream) => {
          //console.log(this.description + ' Polkadot Socket Provider ready');
        });
        that._api.on('error', (stream) => {
          //console.log(this.description + ' Polkadot Socket Provider error');
        });
      });
      
      this.keyring = new Keyring({ type: 'ed25519'});
      this.keyring.setSS58Format(0);
      if(!this.optionsStorage.keypair) {
        let keypair = this.keyring.addFromSeed(randomBytes(32), { name: 'polkadot pair' }, 'ed25519');
        this.optionsStorage.keypair = keypair.toJson();
      }
      // done writing to optionsStorage, save()
      this.save();
      this.keypair = this.keyring.addFromJson(this.optionsStorage.keypair);  
      this.keypair.decodePkcs8();
      super.initialize(app);
    }
    
  }
 /**
  * async getter for Endpoint API
  * @param {Object} app - Saito Application Context
  */
 
  async getApi() {
    if(this._api) {
      await this._api.isReady;
      return this._api;
    } else {
      throw "Cannot get API from inactive module, no API connection was initialized";
    }
  }

  /**
   * Forces input address into the desired format.
   * Input address can be any format, DOT, KSM, or Substrate.
   * https://polkadot.js.org/docs/api/start/create
   * @param {String} address - An address in any subtrate format. See: https://github.com/paritytech/substrate/wiki/External-Address-Format-(SS58)
   * @param {String} format - The desired output format. format ∈ {"polkadot","kusama","substrateRaw"}
   */
  getFormattedAddress(address, format = "polkadot") {
    // https://github.com/paritytech/substrate/wiki/External-Address-Format-(SS58)
    // https://wiki.polkadot.network/docs/en/learn-accounts
    //
    // Polkadot addresses always start with the number 1.
    // Kusama addresses always start with a capital letter like C, D, F, G, H, J...
    // Generic Substrate addresses start with 5.
    //
    // Give some semantics to the polkadot magic numbers
    let formats = {"polkadot": 0, "kusama": 2, "substrateRaw": 42 };
    return encodeAddress(decodeAddress(address), formats[format]);
  }
 /**
  * overrides AbstractCryptoModule returnAddress
  */
  returnAddress() {
    if (this.ticker == "KSM") {
      return this.getFormattedAddress(this.keypair.address, "kusama");
    } else if (this.ticker == "WND") {
      return this.getFormattedAddress(this.keypair.address, "substrateRaw");
    } else {
      return this.getFormattedAddress(this.keypair.address);
    }
  }
 /**
  * overrides AbstractCryptoModule returnPrivateKey
  */
  returnPrivateKey() {
    return "Please remind your module developer to implement this";
    // return this.keypair.something?
  }
 /**
  * overrides AbstractCryptoModule returnBalance
  */
  async returnBalance(){
    let balance = 0.0;
    let api = await this.getApi();
    if(api.query.system.account) {
      const { nonce, data: balanceObj } = await api.query.system.account(this.keypair.publicKey);
      balance = balanceObj.free;
    } else if(api.query.balances.freeBalance) {
      balance = api.query.balances.freeBalance(this.keypair.publicKey);
    }
    if (this.ticker == "WND") {
      return balance/1000000000000.0;
    } else {
      return balance/1.0;
    }
  }
 /**
  * overrides AbstractCryptoModule transfer.
  * 
  * Substrate-based coins have existential deposit (ED) to prevent dust accounts from bloating state. If an account drops below the ED, it will be reaped,
  * Polkadot's ED is 1 DOT, while Kusama's is 0.0016666 KSM.
  */
  async transfer(howMuch, to) {
    if (this.ticker == "WND") {
      howMuch = howMuch*1000000000000;
    }
    let api = await this.getApi();
    const tx = await api.tx.balances.transfer(to, howMuch);
    const hash = await tx.signAndSend(this.keypair);
    return hash;
  }
 /**
  * overrides AbstractCryptoModule hasPayment
  *
  * Only looks in the last 100 payments due to a restriction on subscan's API. This could be extended by repeating the request for past pages.
  */
  hasPayment(howMuch, from, to, timestamp) {
    return new Promise((resolve, reject) => {
      let data = {
        "row": 100,
        "page": 0,
        "address": from
      }
      fetch(this.subscanEndpoint + "/api/scan/transfers", {
          method: 'POST',
          //mode: 'cors',
          headers: {
            'Content-Type': 'application/json',
            'X-API-Key': '014bc513cb936da437a7710d67f27cc7'
          },
          body: JSON.stringify(data)
        })
        .then(response => response.json())
        .then((data) => {
          for(let i = 0; i < data.data.transfers.length; i++) {
            
            let transfer = data.data.transfers[i];
            // DEBUG
            // if(i === 0){
            //   console.log(transfer.from);
            //   console.log(from);
            //   console.log(transfer.from == from);
            // 
            //   console.log(transfer.to);
            //   console.log(to);
            //   console.log(transfer.to == to);
            // 
            //   console.log(transfer.amount);
            //   console.log(howMuch);
            //   console.log(Number.parseFloat(transfer.amount) >= howMuch);
            // 
            //   console.log(transfer.block_timestamp);
            //   console.log(timestamp);
            //   console.log(parseInt(transfer.block_timestamp));
            //   console.log(parseInt(timestamp));
            //   console.log(1000*transfer.block_timestamp > timestamp);
            //   console.log(1000*parseInt(transfer.block_timestamp) > parseInt(timestamp));
            // }
            if(transfer.from == from && transfer.to == to && Number.parseFloat(transfer.amount) >= 0.99*howMuch && 1000*parseInt(transfer.block_timestamp) > parseInt(timestamp)) {
              resolve(true);
              break;
            }
          }
          resolve(false);
        })
        .catch((err) => {
          console.log("Error fetching payments from subscan");
          reject(err);
        });
    });
  }
}
module.exports = SubstrateBasedCrypto;