UNPKG

openchain-sdk-yxl-ts

Version:

OpenChain SDK for browser

875 lines (760 loc) 21.1 kB
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;