apla-blockchain-tools
Version:
Module contains a number of tools to work with Apla Blockchain
276 lines (237 loc) • 8.65 kB
JavaScript
;
// MIT License
//
// Copyright (c) 2016-2018 AplaProject
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
/*
* Package: apla-blockchain-tools
* Author: Anton Zuev
* mail: a.zuev@apla.io
* Company: apla.io
*/
let fetch = require("node-fetch");
let keyring = require("../keyring");
let formData = require('form-data');
let Contract = require("./contract");
let {setTimeoutPromise} = require("../lib/utils");
let {getError, getTransactionError} = require("./errors");
// Default: how many times script tries to get result of the transaction
const TX_STATUS_MAX_TRIES = 20;
// Default: how many ms to wait before next try to get tx result
const WAIT_BEFORE_NEXT_TRY = 1000;
/**
* Class for interacting with the Apla Netowrk
*/
class Session {
/**
*
* @param apiUrl {string} - url to connect to
* @param networkId {string} - id of the network
* @param options {object}
* @param options.privateKey {string} - private key
* @param options.maxSum {number | string} - not used for now
* @param options.payOver {number | string} - not used for now
* @param options.txStatusMaxTries {number} - how many times client will try to get the result
* @param options.waitBeforeNextTry {number} - how many ms client has to wait before next try to get the result of tx
*/
constructor(apiUrl, networkId, options = {}) {
this.apiUrl = apiUrl;
this.networkId = Number.parseInt(networkId);
this.keys = {
private: options.privateKey
};
if (this.keys.private) {
this.keys.public = keyring.generatePublicKey(this.keys.private, true);
}
this.consts = {
maxSum: options.maxSum || 1000 * 1000,
payOver: options.payOver || 0,
txStatusMaxTries: options.txStatusMaxTries || TX_STATUS_MAX_TRIES,
waitBeforeNextTry: options.waitBeforeNextTry || WAIT_BEFORE_NEXT_TRY
}
}
/**
* generates new private key and corresponding public key
* @public
*/
generateKeys() {
let seed = keyring.generateSeed();
this.keys = keyring.generateKeyPair(seed);
// trim (remove 04 from the beginning)
this.keys.public = this.keys.public.slice(2);
}
/**
* login to the system
* @public
* @param options
* @param options.ecosystem {number | string} - ecosystem to login to
* @param options.roleId {number | string} - login to login to
*/
async login(options = {}) {
this.ecosystemId = options.ecosystem || 1;
this.roleId = options.roleId || "";
let resp = await fetch(`${this.apiUrl}/getuid`);
let {uid, token} = await resp.json();
let form = new formData();
form.append("pubkey", this.keys.public);
form.append("signature", keyring.sign(`LOGIN${this.networkId}${uid}`, this.keys.private));
form.append('ecosystem', this.ecosystemId);
form.append('role_id', this.roleId);
resp = await fetch(`${this.apiUrl}/login`, {
method: "POST",
body: form,
headers: {
"content-type": form.getHeaders()['content-type'],
authorization: `Bearer ${token}`
}
});
let res = await resp.json();
let err = getError(res);
if(err){
throw err;
}
this.token = res.token;
this.keyId = res.key_id;
this.address = res.address;
}
/**
* Send signed tx
* @private
* @param tx
* @param hash
* @return {{hashes: *}}
*/
async sendTx(tx, hash) {
let form = new formData();
let txBuffer = Buffer.from(tx);
form.append(hash, txBuffer, {
filename: hash
});
let response = await fetch(`${this.apiUrl}/sendTx`, {
method: "POST",
body: form,
headers: {
authorization: `Bearer ${this.token}`,
"content-type": form.getHeaders()['content-type']
}
}
);
return await response.json();
}
/**
* getting information about the contract
* @private
* @param contractName
* @throws {ContractNotFoundError} if there is no such contract
* @return {Promise.<*>}
*/
async getContractSchema(contractName) {
let response = await fetch(`${this.apiUrl}/contract/${contractName}`, {
method: "GET",
headers: {
authorization: `Bearer ${this.token}`
}
}
);
let json = await response.json();
let error = getError(json);
if(error){
throw error;
}
return json;
}
/**
* Waiting for the sent transaction to be executed
* @param hash (for now supports only one hash at a time)
* @return {object}
*
*/
async waitTxStatus(hash){
let counter = 0;
while(counter < this.consts.txStatusMaxTries){
let form = new formData();
let obj = {
hashes: [hash]
};
form.append("data", JSON.stringify(obj));
let response = await fetch(`${this.apiUrl}/txstatus`, {
method: "POST",
body: form,
headers: {
authorization: `Bearer ${this.token}`,
"content-type": form.getHeaders()['content-type']
}
});
let {results} = await response.json();
let txRes = results[hash];
let err = getTransactionError(txRes);
if(err) throw err;
if(txRes.blockid){
return txRes;
}
await setTimeoutPromise(this.consts.waitBeforeNextTry);
counter++;
}
throw new Error(`tx #${hash}: tx wasn't included in the block. Tried ${counter} times`);
}
/**
* Call the contract by name
* @public
* @param name {string} - name of the contract
* @param params {object} - name->value mapping
* @param options
* @param options.maxSum {string | number } - max cost of transaction
* @param options.payOver {string | number } - (fuelRate + options.payOver) * fuelVolume
* @return {object}
*/
async callContract(name, params, options = {
maxSum: this.consts.maxSum,
payOver: this.consts.payOver
}) {
let contractSchema = await this.getContractSchema(name);
options.keyId = this.keyId;
options.ecosystemId = this.ecosystemId;
options.networkId = this.networkId;
let contract = new Contract(contractSchema, params, options);
let {hash, data} = contract.sign(this.keys.private);
let {hashes} = await this.sendTx(data, hash);
return await this.waitTxStatus(hash);
}
/**
* returns list of blocks of size `count` from block with id = `blockId`
* @public
* @param blockId {number | string}
* @param count {number}
* @return {Array}
*/
async getDetailedBlocks(blockId, count) {
return await fetch(`${this.apiUrl}/detailed_blocks?block_id=${blockId}&count=${count}`);
}
/**
* returns id of the last block for the node
* @public
* @return {number}
*/
async getMaxBlockId() {
let response = await fetch(`${this.apiUrl}/maxblockid`);
return (await response.json())['max_block_id']
}
}
module.exports = Session;