openchain-sdk-yxl-ts
Version:
OpenChain SDK for browser
875 lines (760 loc) • 21.1 kB
JavaScript
import axios from 'axios';
import long from 'long';
import JSONbig from 'json-bigint';
import BigNumber from 'bignumber.js';
import protobuf from "protobufjs";
import bundleJson from '../crypto/protobuf/bundle.js';
import humps from 'humps';
import toUint8Array from './buffer-utils.js';
import { keypair, signature } from 'openchain-encryption-nodejs-yxl-ts';
import errors from '../exception/index.js';
import { Buffer } from 'buffer';
import OperationModule from'./operation/index.js'
class CommonUtil {
/**
* Initialize CommonUtil
* @param {Object} options - Configuration options
* @param {string} options.host - API host URL
*/
constructor(options) {
if (!options || typeof options !== 'object' || options === null) {
throw new Error('options must be an object');
}
if (typeof options.host !== 'string') {
throw new Error('options.host must be a non-empty string');
}
this.options = options;
}
/**
* GET/POST request
*
* @param {String} method
* @param {String} path
* @param {Object} data
* @return {Object}
*/
async _request(method, endpoint, paramsAndData) {
let params = {};
let data = {};
if (method === 'get') {
params = paramsAndData;
} else {
data = paramsAndData;
}
try {
const response = await axios({
method,
url: `${this.options.host}/${endpoint}`,
params,
data,
});
return response.data;
} catch (error) {
throw this._handleRequestError(error);
}
}
_response(obj) {
const data = {
errorCode: obj.error_code || 0,
errorDesc: obj.error_desc || 'Success',
};
if (typeof obj === 'object' && obj !== null && obj.error_code) {
if (obj.error_code === 0) {
data.result = obj.result || {};
} else {
data.errorDesc = obj.error_desc || '';
data.result = {};
}
} else {
data.result = obj;
}
return JSONbig.stringify(data);
}
async _getBlockNumber() {
try {
const data = await this._request('get', 'getLedger');
if (data && data.error_code === 0) {
const seq = data.result.header.seq;
return this._responseData({
header: {
blockNumber: seq,
},
});
} else {
return this._responseError(errors.INTERNAL_ERROR);
}
} catch (err) {
throw err;
}
}
_isEmptyString(str) {
if (typeof str !== 'string') {
throw new Error('str must be a string');
}
return (str.trim().length === 0);
}
_postData(blob, signature) {
const data = {
items: [
{
transaction_blob: blob,
signatures: signature
},
],
};
return JSONbig.stringify(data);
}
_isBigNumber(object) {
return object instanceof BigNumber ||
(object && object.constructor && object.constructor.name === 'BigNumber');
}
_toBigNumber(number) {
number = number || 0;
if (this._isBigNumber(number)) {
return number;
}
return new BigNumber(number);
}
_stringFromBigNumber(number) {
return this._toBigNumber(number).toString(10);
}
_verifyValue(str) {
const reg = /^[1-9]\d*$/;
return (
typeof str === 'string' &&
reg.test(str) &&
long.fromValue(str).greaterThan(0) &&
long.fromValue(str).lessThanOrEqual(long.MAX_VALUE)
);
}
_isAvailableValue(str, from = -1, to = long.MAX_VALUE) {
const reg = /^(0|([1-9]\d*))$/;
return (
typeof str === 'string' &&
reg.test(str) &&
long.fromValue(str).greaterThan(from) &&
long.fromValue(str).lessThanOrEqual(to)
);
}
_checkParams(obj) {
for (let prop in obj) {
if (obj.hasOwnProperty(prop)) {
let value = obj[prop];
if (value !== undefined) {
if (!this._verifyValue(value)) {
throw new Error(errors.INVALID_FORMAT_OF_ARG.msg);
}
}
}
}
}
async _getDefaultValue() {
try {
let ledgerInfo = await this._request('get', 'getLedger', {
with_fee: true,
});
const gasPrice = long.fromValue(ledgerInfo.result.fees.gas_price);
const feeLimit = long.fromValue(1000).mul(gasPrice);
return {
gasPrice,
feeLimit,
};
} catch (err) {
throw err;
}
}
_responseData(data) {
const errorCode = 0;
const errorDesc = '';
return {
errorCode,
errorDesc,
result: data,
};
}
_responseError(message) {
if (!message) {
throw new Error('require message');
}
const errorCode = message.CODE;
return {
errorCode,
errorDesc: message.MSG,
};
}
async _submitTransaction(data) {
try {
const res = await this._request('post', 'submitTransaction', data);
const results = res.results;
if (Array.isArray(results) && results.length > 0) {
const info = results[0];
if (info.error_code === 0) {
return this._responseData({
hash: info.hash,
});
}
return {
errorCode: info.error_code,
errorDesc: info.error_desc,
};
}
} catch (err) {
throw err;
}
}
_buildOperation(type, data) {
if (!type || !data) {
throw new Error('Operation type and data are required');
}
try {
const operationModule = OperationModule(type);
if (!operationModule) {
throw new Error(`Operation type '${type}' is not supported`);
}
return operationModule(data);
} catch (err) {
throw new Error(`Failed to build operation: ${err.message}`);
}
}
_decodeOperation(hexString) {
const root = protobuf.Root.fromJSON(bundleJson);
const operation = root.lookupType('protocol.Operation');
const msgBuffer = Buffer.from(hexString, 'hex');
return operation.decode(msgBuffer);
}
_buildBlob(args) {
try {
let { sourceAddress, gasPrice, feeLimit, nonce, ceilLedgerSeq, operations, metadata } = args;
const operationList = [];
operations.forEach(item => {
const type = item.type;
const argsData = item.data;
const operationItem = this._buildOperation(type, argsData);
operationList.push(operationItem);
});
const root = protobuf.Root.fromJSON(bundleJson);
const tx = root.lookupType('protocol.Transaction');
ceilLedgerSeq = ceilLedgerSeq ? long.fromValue(ceilLedgerSeq) : undefined;
const payload = {
sourceAddress,
gasPrice: long.fromValue(gasPrice),
feeLimit: long.fromValue(feeLimit),
nonce: long.fromValue(nonce),
ceilLedgerSeq,
operations: operationList,
};
if (metadata) {
payload.metadata = toUint8Array(Buffer.from(metadata));
}
const errMsg = tx.verify(payload);
if (errMsg) {
throw Error(errMsg);
}
const message = tx.create(payload);
const bufferData = tx.encode(message).finish();
return {
// transactionBlob: bufferData,
transactionBlob: Buffer.from(bufferData).toString('hex'),
};
} catch (err) {
throw err;
}
}
_signBlob({ privateKeys, blob } = args) {
try {
const buffer = Buffer.from(blob, 'hex');
const uint8ArrayData = toUint8Array(buffer);
const signatureArr = [];
privateKeys.forEach(privateKey => {
signatureArr.push({
signData: signature.sign(uint8ArrayData, privateKey),
publicKey: keypair.getEncPublicKey(privateKey),
});
});
return {
signatures: signatureArr,
};
} catch (err) {
throw err;
}
}
async _submit(args) {
const { blob, signature } = args;
const postData = this._postData(blob, signature);
return await this._submitTransaction(postData);
}
_isHexString(str) {
if ((str === '' || typeof str !== 'string')) {
return false;
}
return true;
}
_isString(str) {
if (typeof str !== 'string' ||
str.trim().length === 0 ||
str.length > 1024) {
return false;
}
return true;
}
_isTopic(str) {
if (typeof str !== 'string' ||
str.trim().length === 0 ||
str.length > 128) {
return false;
}
return true;
}
_isSignature(arr) {
let tag = true;
if (!Array.isArray(arr) || arr.length === 0) {
tag = false;
return tag;
}
arr.some(item => {
if (typeof item !== 'object' || item === null) {
tag = false;
return true;
}
if (!item.signData || !item.publicKey) {
tag = false;
return true;
}
if (!this._isHexString(item.signData)) {
tag = false;
return true;
}
if (!keypair.checkEncPublicKey(item.publicKey)) {
tag = false;
return true;
}
});
return tag;
}
_isOperation(arr) {
let tag = true;
if (!Array.isArray(arr) || arr.length === 0) {
tag = false;
return tag;
}
arr.some(item => {
if (typeof item !== 'object' || item === null) {
tag = false;
return true;
}
if (!item.type || !item.data) {
tag = false;
return true;
}
});
return tag;
}
_isDatas(arr) {
let tag = true;
if (!Array.isArray(arr) || arr.length === 0) {
tag = false;
return tag;
}
arr.some(item => {
if (typeof item !== 'string' ||
item.trim().length === 0 ||
item.length > 1024) {
tag = false;
return true;
}
});
return tag;
}
_isPrivateKeys(arr) {
let tag = true;
if (!Array.isArray(arr) || arr.length === 0) {
tag = false;
return tag;
}
arr.some(item => {
if (!keypair.checkEncPrivateKey(item)) {
tag = false;
return true;
}
});
return tag;
}
_validate(obj, schema) {
let tag = true;
let msg = '';
if (typeof obj !== 'object' || obj === null || typeof schema !== 'object' || schema === null) {
tag = false;
msg = 'INVALID_NUMBER_OF_ARG';
return {
tag,
msg,
};
}
Object.keys(schema).some(item => {
// required is true
if (schema[item].required && obj[item] === undefined) {
obj[item] = '';
}
// numeric is true
if (obj[item] !== undefined &&
schema[item].numeric &&
!this._verifyValue(obj[item])) {
tag = false;
switch (item) {
case 'amount':
msg = 'INVALID_OP_AMOUNT_ERROR';
break;
case 'opAmount':
msg = 'INVALID_OP_AMOUNT_ERROR';
break;
case 'assetAmount':
msg = 'INVALID_ASSET_AMOUNT_ERROR';
break;
case 'gasPrice':
msg = 'INVALID_GASPRICE_ERROR';
break;
case 'feeLimit':
msg = 'INVALID_FEELIMIT_ERROR';
break;
case 'ceilLedgerSeq':
msg = 'INVALID_CEILLEDGERSEQ_ERROR';
break;
case 'nonce':
msg = 'INVALID_NONCE_ERROR';
break;
case 'initBalance':
msg = 'INVALID_INITBALANCE_ERROR';
break;
case 'signtureNumber':
msg = 'INVALID_SIGNATURENUMBER_ERROR';
break;
case 'totalSupply':
msg = 'INVALID_TOKEN_TOTALSUPPLY_ERROR';
break;
case 'tokenAmount':
msg = 'INVALID_TOKEN_AMOUNT_ERROR';
break;
default:
msg = 'INVALID_ARGUMENTS';
}
return true;
}
// privateKeys is true
if (obj[item] !== undefined &&
schema[item].privateKeys &&
!this._isPrivateKeys(obj[item])) {
tag = false;
msg = `PRIVATEKEY_ONE_ERROR`;
return true;
}
// address is true
if (obj[item] !== undefined &&
schema[item].address &&
!keypair.checkAddress(obj[item])) {
tag = false;
switch (item) {
case 'sourceAddress':
msg = 'INVALID_SOURCEADDRESS_ERROR';
break;
case 'destAddress':
msg = 'INVALID_DESTADDRESS_ERROR';
break;
case 'issuer':
msg = 'INVALID_ISSUER_ADDRESS_ERROR';
break;
case 'address':
msg = 'INVALID_ADDRESS_ERROR';
break;
case 'contractAddress':
msg = 'INVALID_CONTRACTADDRESS_ERROR';
break;
case 'fromAddress':
msg = 'INVALID_FROMADDRESS_ERROR';
break;
case 'spender':
msg = 'INVALID_SPENDER_ERROR';
break;
case 'tokenOwner':
msg = 'INVALID_TOKENOWNER_ERRPR';
break;
default:
msg = 'INVALID_ARGUMENTS';
}
return true;
}
// operations is true
if (obj[item] !== undefined &&
schema[item].operations &&
!this._isOperation(obj[item])) {
tag = false;
msg = 'INVALID_OPERATIONS';
return true;
}
// signatures is true
if (obj[item] !== undefined &&
schema[item].signatures &&
!this._isSignature(obj[item])) {
tag = false;
msg = 'INVALID_SIGNATURE_ERROR';
return true;
}
// hex is true
if (obj[item] !== undefined &&
schema[item].hex &&
!this._isHexString(obj[item])) {
tag = false;
switch (item) {
case 'metadata':
msg = 'METADATA_NOT_HEX_STRING_ERROR';
break;
case 'blob':
msg = 'INVALID_BLOB_ERROR';
break;
default:
msg = 'METADATA_NOT_HEX_STRING_ERROR';
}
return true;
}
// string is true
if (obj[item] !== undefined &&
schema[item].string &&
!this._isString(obj[item])) {
tag = false;
switch (item) {
case 'code':
msg = 'INVALID_ASSET_CODE_ERROR';
break;
case 'issuer':
msg = 'INVALID_ISSUER_ADDRESS_ERROR';
break;
case 'data':
msg = 'INVALID_LOG_DATA_ERROR';
break;
case 'metadata':
msg = 'INVALID_METADATA_ERROR';
break;
case 'payload':
msg = 'PAYLOAD_EMPTY_ERROR';
break;
case 'input':
msg = 'INVALID_INPUT_ERROR';
break;
case 'name':
msg = 'INVALID_TOKEN_NAME_ERROR';
break;
case 'symbol':
msg = 'INVALID_TOKEN_SYMBOL_ERROR';
break;
case 'key':
msg = 'INVALID_DATAKEY_ERROR';
break;
default:
msg = 'INVALID_ARGUMENTS';
}
return true;
}
// topic is true
if (obj[item] !== undefined &&
schema[item].topic &&
!this._isTopic(obj[item])) {
tag = false;
msg = 'INVALID_LOG_TOPIC_ERROR';
return true;
}
// datas is true
if (obj[item] !== undefined &&
schema[item].datas &&
!this._isDatas(obj[item])) {
tag = false;
msg = `INVALID_LOG_DATAS_ERROR`;
return true;
}
// boolean is true
if (obj[item] !== undefined &&
schema[item].boolean &&
(typeof obj[item] !== 'boolean')) {
tag = false;
switch (item) {
case 'deleteFlag':
msg = 'INVALID_DELETEFLAG_ERROR';
break;
default:
msg = 'INVALID_ARGUMENTS';
}
return true;
}
});
return {
tag,
msg,
};
}
_bufToHex(buf) {
const utf8Str = buf.toString('utf8');
return Buffer.from(utf8Str, 'utf8').toString('hex');
}
_bigNumberToString(obj, base = 10) {
// check if obj is type object, not an array and does not have BN properties
if (typeof obj === 'object' && obj !== null && !Array.isArray(obj) && !('lessThan' in obj)) {
// move through plain object
Object.keys(obj).forEach((key) => {
// recursively convert item
obj[key] = this._bigNumberToString(obj[key], base);
});
}
// obj is an array
if (Array.isArray(obj)) {
// convert items in array
obj = obj.map((item) => this._bigNumberToString(item, base));
}
// if obj is number, convert to string
if (typeof obj === 'number') return obj.toString();
// if not an object bypass
if (typeof obj !== 'object' || obj === null) return obj;
// if the object does not have BigNumber properties, bypass
if (!('toString' in obj) || !('lessThan' in obj)) return obj;
// if object has bignumber properties, convert to string with base
return obj.toString(base);
}
_longToInt(obj) {
// check if obj is type object, not an array and does not have long properties
if (typeof obj === 'object' && obj !== null && !Array.isArray(obj) && !('low' in obj)) {
// move through plain object
Object.keys(obj).forEach((key) => {
// recursively convert item
obj[key] = this._longToInt(obj[key]);
});
}
// obj is an array
if (Array.isArray(obj)) {
// convert items in array
obj = obj.map((item) => this._longToInt(item));
}
// if not an object bypass
if (typeof obj !== 'object' || obj === null) return obj;
// if the object does not have long properties, bypass
if (!('low' in obj)) return obj;
// if object has long properties, convert to int
return long.fromValue(obj).toNumber();
}
async _isContractAddress(address) {
const data = await this._request('get', 'getAccount', {
address,
});
if (data.error_code !== 0) {
return this._responseError(errors.ACCOUNT_NOT_EXIST);
}
const result = data.result;
if (result.contract) {
return true;
}
return false;
}
async _isAvailableToken(contractAddress) {
if (!keypair.checkAddress(contractAddress)) {
return this._responseError(errors.INVALID_CONTRACTADDRESS_ERROR);
}
const isContractAddress = await this._isContractAddress(contractAddress);
if (!isContractAddress) {
return this._responseError(errors.CONTRACTADDRESS_NOT_CONTRACTACCOUNT_ERROR);
}
let data = await this._request('get', 'getAccount', {
address: contractAddress,
});
if (data.error_code !== 0) {
return this._responseData({
isValid: false,
});
}
data = data.result;
const contract = data.contract.metadatas;
const metadatas = data.metadatas;
let key = '';
let value = '';
if (metadatas && Array.isArray(metadatas)) {
metadatas.some(item => {
if (item.key === 'global_attribute') {
key = 'global_attribute';
value = item.value;
return true;
}
});
if (key !== 'global_attribute') {
return this._responseData({
isValid: false,
});
}
const info = JSON.parse(value);
if ('1.0' !== info.ctp) {
return this._responseData({
isValid: false,
});
}
if (!info.symbol || info.symbol < 0 || info.symbol > 8) {
return this._responseData({
isValid: false,
});
}
const schema = {
balance: {
required: true,
numeric: true,
},
name: {
required: true,
string: true,
},
symbol: {
required: true,
string: true,
},
totalSupply: {
required: true,
numeric: true,
},
contractOwner: {
required: true,
address: true,
},
};
if (!this._validate(info, schema).tag) {
return this._responseData({
isValid: false,
});
}
return this._responseData({
isValid: true,
});
} else {
return this._responseData({
isValid: false,
});
}
}
_isAvailableVersion(str) {
const reg = /^\d+(\.\d+)?$/;
return (
typeof str === 'string' &&
reg.test(str) &&
long.fromValue(str).greaterThanOrEqual(0) &&
long.fromValue(str).lessThanOrEqual(long.MAX_VALUE)
);
}
_isAvailableOP(str) {
const reg = /^(([1-9]\d*)+|0)(\.\d{1,8})?$/;
return (
typeof str === 'string' &&
reg.test(str) &&
long.fromValue(str).greaterThanOrEqual(0) &&
long.fromValue(str).lessThanOrEqual(long.MAX_VALUE.divide(Math.pow(10, 8)))
);
}
_handleRequestError(error) {
if (error.response) {
const { status, data } = error.response;
return new Error(`请求失败: 状态码 ${status}, 响应数据: ${JSON.stringify(data)}`);
}
return new Error(`网络连接失败: ${error.message}`);
}
}
export default CommonUtil;