import {utils} from '@womasoft/js-utils'; import * as qitmeer from 'qitmeer-js'; import {error, IReturnResult, networkType, Return, txListStat, txListType} from '../../config'; import {mnemonicTools} from '../../mnemonic' import { IChainsParams, IChainsVerify, IContractDetails, IFromMnemonicParams, IGetBalanceParams, ISignParams, IToWallet, ITxDetails, ITxList, ITxListParams, ITxListToList, IVerifyBalanceParams } from '../chains'; import {IApi, IQitmeerBalance, IQitmeer, IQitmeerSendTxParams, IQitmeerSignEvmParams} from "./qitmeer"; import {defaultParams} from "./config"; class Qitmeer implements IQitmeer { // 接收访问参数 private readonly _params: IChainsParams constructor(params) { this._params = params ?? defaultParams; if (this._params.api && utils.tools.isStrLast(this._params.api, '/')) this._params.api = utils.tools.toStrFirst(this._params.api, this._params.api.length - 1); this._params.network = this._params.network ?? networkType.mainnet; } async verifyBalance(address: string, amount: string, params: IVerifyBalanceParams | undefined, hasUnlock?: boolean): Promise> { params = params || {}; const ReturnClass = new Return(); if (!params.fee) params.fee = '0'; if (!utils.tools.isAmount(amount)) return error.amount; const mainGetBalance = await this.getBalance(address); // 主币金额 let amountMain = mainGetBalance.code > 0 ? 0 : Number(mainGetBalance.rs.usable); // 可划转金额 let reverseUsable = mainGetBalance.code > 0 ? 0 : Number(mainGetBalance.rs.reverseUsable); // 对比金额 const amountValue = Number(amount); // 手续费 const feeValue = Number(params.fee); if (params.contract && params.contract.indexOf('0x') === 0) { const tokenBalance = await this.getBalance(address, { contract: params.contract, decimals: params.decimals }); const amountToken = tokenBalance.code > 0 ? 0 : Number(tokenBalance.rs.usable); if (amountToken < amountValue) return error.amountNotEnough; if (amountMain < feeValue) return error.feeNotEnough; } else { if (hasUnlock) { if ((amountMain + reverseUsable) < amountValue) return error.amountNotEnough; if (feeValue > 0) amountMain = utils.math.plus(amountMain, feeValue); if ((amountMain + reverseUsable) < amountValue) return error.feeNotEnough; } else { if (amountMain < amountValue) return error.amountNotEnough; if (feeValue > 0) amountMain = utils.math.plus(amountMain, feeValue); if (amountMain < amountValue) return error.feeNotEnough; } } return ReturnClass.end(); } readonly verify: IChainsVerify = { address: (address: string): boolean => { address = address || ""; if (address.length !== 35) return false; return (this._params.network === networkType.mainnet && address.indexOf('Mm') === 0) || (this._params.network === networkType.testnet && address.indexOf('Tn') === 0) }, privateKey(privateKey: string): boolean { privateKey = privateKey || ""; return privateKey.length === 64; }, publicKey(publicKey: string): boolean { publicKey = publicKey || ""; return publicKey.length === 66; }, balance: async (address: string, amount: string, params: IVerifyBalanceParams | undefined): Promise> => { return this.verifyBalance(address, amount, params); }, }; async fromMnemonicToWallet(mnemonic: string, params?: IFromMnemonicParams | undefined): Promise> { params = params || {}; const data = await mnemonicTools.toSeed(mnemonic, params.path); if (data.code > 0) return data; return this.fromPrivateKeyToWallet(data.privateKey); } fromPrivateKeyToWallet(privateKey: string): IReturnResult { const ReturnClass = new Return(); privateKey = privateKey.indexOf('0x') === 0 ? privateKey.substring(2, privateKey.length) : privateKey; if (!this.verify.privateKey(privateKey)) return error.privateKey; const publicKey = this.getPublicKey(privateKey); const address = this.getAddress(publicKey); const data: IToWallet = { address, publicKey, privateKey, } ReturnClass.setPrivateKey(privateKey); ReturnClass.setPublicKey(publicKey); ReturnClass.setAddress(address); ReturnClass.setResult(data); return ReturnClass.end() } async fromRandomToWallet(): Promise> { const wallet = qitmeer.ec.fromEntropy(); const privateKey = wallet.privateKey.toString('hex'); return this.fromPrivateKeyToWallet(privateKey); } getFee(utxoLength: number): string { const feeRatio = { quick: 3, // 快速 general: 2, // 一般 slow: 1, // 较慢 } // 计算手续费 const one = utils.math.multipliedBy(utxoLength - 1, 1600); const two = utils.math.multipliedBy(utxoLength + 1, 500); let fee = utils.math.plus(one, two); fee = utils.math.plus(fee, 2000); // 计算比例 fee = utils.math.multipliedBy(fee, feeRatio.general); return utils.math.dividedBy(fee, 1e8).toString(); } getAddress(value: string): string { const publicBuff = typeof value === 'string' ? Buffer.from(value, 'hex') : value; // const hash160 = qitmeer.hash.hash160(publicBuff); return qitmeer.address.ecToPkHAddress(publicBuff, this._params.network); } getKeyPair(privateKey: string): any { const privateBuff = typeof privateKey === 'string' ? Buffer.from(privateKey, 'hex') : privateKey; return qitmeer.ec.fromPrivateKey(privateBuff); } getPKAddress(publicKey: string): string { const publicBuff = typeof publicKey === 'string' ? Buffer.from(publicKey, 'hex') : publicKey; return qitmeer.address.ecToPkAddress(publicBuff, this._params.network); } getPath(index?: number | undefined): string { index = index ?? 0; // let path = "m/44'/813'/0'/0/{index}"; let path = "m/44'/223'/0'/0/{index}"; if (!utils.tools.isInteger(index)) index = 0; return utils.tools.replaceByJson(path, {index}); } getPublicKey(privateKey: string): string { const keyPair = this.getKeyPair(privateKey); return keyPair.publicKey.toString('hex'); } async getAllBalance(address: string): Promise> { const ReturnClass = new Return<{ [key: string]: string }>(); if (!this.verify.address(address)) return error.address; const mainBalance = await this.getBalance(address); const list = {}; if (mainBalance.code === 0) list[this._params.symbol] = mainBalance.rs.balance; ReturnClass.setResult(list); return ReturnClass.end(); } async getBalance(address: string, params?: IGetBalanceParams | undefined): Promise> { params = params ?? {}; const ReturnClass = new Return(); if (!this.verify.address(address)) return error.address; const data = await this.api.getBalance(address, params.contract); if (data.code > 0) { console.log('err', data); return error.networksTimeout } ReturnClass.setResult({ balance: data.rs.balance.toString(), usable: data.rs.usable.toString(), lock: data.rs.locked.toString(), unconfirmed: data.rs.unconfirmed.toString(), reverseUsable: data.rs.reverseUsable ? data.rs.reverseUsable.toString() : '0', // 可划转余额 reverseLocked: data.rs.reverseLocked ? data.rs.reverseLocked.toString() : '0', // 需解锁后划转余额 }) return ReturnClass.end(); } getBlockHeight(): Promise> { return Promise.resolve(undefined); } getContractDetails(): Promise> { return Promise.resolve(undefined); } async getTxDetailsByHash(hash: string, address?: string): Promise> { const ReturnClass = new Return(); const data = await this.api.getTxDetails(hash, address ?? ''); const from = data.rs.vins[0].address; const to = data.rs.vouts[0].address; let params: ITxDetails = { type: txListType.init, stat: txListStat.init, hash: hash, blockHash: '', blockNumber: '0', from, to, fee: data.rs.fees, nonce: '0', value: data.rs.vins[0].amount, time: utils.tools.formatDate(data.rs.timestamp * 1000), timestamp: (data.rs.timestamp * 1000).toString(), }; if (data.rs.stat === 0) params.stat = txListStat.success; else if (data.rs.stat === 3) params.stat = txListStat.fail; ReturnClass.setResult(params); return ReturnClass.end(); } async getTxList(address: string, params?: ITxListParams | undefined): Promise> { params = params || {} const ReturnClass = new Return(); if (!this.verify.address(address)) return error.address; params.type = params.type || 'all'; params.size = params.size || 30 params.page = params.page || 1; let result: ITxList[] = []; let data = await this.api.getTxList(params.type, address, params.contract, params.size, params.page); if (data.rs.result && data.rs.result.length) data.rs.result.map((item) => { const toList: ITxListToList[] = []; const resultParams: ITxList = { type: txListType.init, stat: txListStat.init, hash: item.txid, from: '', to: toList, value: item.change, timestamp: item.timestamp, time: utils.tools.formatDate(utils.math.multipliedBy(item.timestamp, 1000)), fee: '0', contractAddress: item.contract, contractDecimals: item.contractDecimals, contractSymbol: this._params.symbol, contractName: item.contractName, } if (item.address === null) resultParams.from = undefined; // 发送方式 if (item.change > 0) { // 转入 resultParams.type = txListType.in; resultParams.from = item.address ? item.address[0] : ''; resultParams.to.push({address, amount: '0'}); } else { // 转出 resultParams.type = txListType.out; resultParams.from = address; resultParams.to.push({address: item.address ? item.address[0] : '', amount: '0'}); } // 交易状态 if (item.stat === 0) resultParams.stat = txListStat.success; else if (item.stat === 1) resultParams.stat = txListStat.confirming; else if (item.stat === 2) resultParams.stat = txListStat.unconfirmed; else if (item.stat === 3) resultParams.stat = txListStat.fail; else if (item.stat === 4) resultParams.stat = txListStat.fail; result.push(resultParams); }) if (params.type === 'in') result = result.filter(m => m.type === txListType.in); if (params.type === 'out') result = result.filter(m => m.type === txListType.out); ReturnClass.setResult(result); return ReturnClass.end(); } async sendTx(value: string, params?: IQitmeerSendTxParams): Promise> { params = params || {}; const ReturnClass = new Return(); const data = await this.api.sendTx(value, params.address, params.note); const {code, rs, msg} = data; if (code === 0) { ReturnClass.setResult(rs); } else { console.error(rs, msg); return error.wait; } return ReturnClass.end(); } async sign(from: string, to: string, amount: string, params?: ISignParams | undefined): Promise> { const ReturnClass = new Return(); params = params || {}; const _network = qitmeer.networks[this._params.network]; if (!this.verify.address(to)) return error.address; if (!this.verify.address(from)) return error.address; // 获取主币 utxo const getUTXO = await this.api.getUTXO(from, params.contract, amount, '2', params.hasUnlock ? 'reserveLocked' : undefined); const utxo = getUTXO.rs ?? {}; if (!utxo.outs || utxo.outs.length === 0) return error.amountNotEnough; let coinId = 0; let utxoMain: any = {}; if (params.contract) { const coinList = await this.api.getCoinIdList(); const coinItem = coinList.rs.find(m => m.coinName === params.contract); if (!coinItem) return error.contract; coinId = coinItem.coinId; const getMainUtxo = await this.api.getUTXO(from, undefined, '1', '1'); utxoMain = getMainUtxo.rs ?? {}; } // 获取手续费 const fee = this.getFee(utxo.outs.length); // 解锁可划转金额 if (params.hasUnlock) amount = utils.math.minus(amount, fee).toString(); // 验证余额手续费 const verifyBalance = await this.verify.balance(from, amount, {fee}); if (verifyBalance.code > 0) return verifyBalance; if (params.privateKey) { if (!this.verify.privateKey(params.privateKey)) return error.privateKey; const walletData = this.fromPrivateKeyToWallet(params.privateKey); if (walletData.address !== from) return error.privateKey; // 初始化签名 const TxBuilder = new qitmeer.txsign.newSigner(_network); TxBuilder.setVersion(1); TxBuilder.setTimestamp(Math.ceil(new Date().getTime() / 1e3)); // TxBuilder.setLockTime(0); if (utxo.height) TxBuilder.setLockTime(utxo.height); const amountTotal = utils.math.plus(amount, coinId === 0 ? fee : 0); // 循环写入数据 let utxoList = []; const utxoData = this._getSignUTXO(utxo.outs, amountTotal.toString()); if (utxoData.code > 0) return utxoData; utxo.amount = utxoData.amount; utxoList = utxoList.concat(utxoData.rs); // 合约 添加 MEER手续费 if (coinId > 0) { const utxoMainData = this._getSignUTXO(utxoMain.outs, fee); if (utxoMainData.code > 0) return utxoMainData; utxoMain.amount = utxoMainData.amount; utxoList = utxoList.concat(utxoMainData.rs); } for (let i = 0, len = utxoList.length; i < len; i++) { // 锁定 let options; if (utxoList[i].lockheight > 0) options = { prevOutType: qitmeer.script.Output.CLTV, //锁定脚本 lockTime: utxoList[i].lockheight, //锁定高度 sequence: qitmeer.tx.DEFAULT_SEQUENCE - 1, //锁定***** } // console.log('addInput', utxoList[i].txid, utxoList[i].number, options) TxBuilder.addInput(utxoList[i].txid, utxoList[i].number, options); } // 添加 addOutput const amountBack = utils.math.minus(utxo.amount, amountTotal); const amountDecimal = utils.math.multipliedBy(amount, 1e8); const amountBackDecimals = utils.math.multipliedBy(amountBack, 1e8); TxBuilder.addOutput(to, amountDecimal, coinId) if (amountBackDecimals > 0) TxBuilder.addOutput(from, amountBackDecimals, coinId); // 添加 手续费 addOutput if (coinId > 0) { const feeBack = utils.math.minus(utxoMain.amount, fee); const feeBackDecimals = utils.math.multipliedBy(feeBack, 1e8); // console.log('addOutput 手续费', from, feesBackDecimals, 0); if (feeBackDecimals > 0) TxBuilder.addOutput(from, feeBackDecimals, 0); } const keyPair = this.getKeyPair(params.privateKey); for (let i = 0, len = utxoList.length; i < len; i++) { TxBuilder.sign(i, keyPair); } const raw = TxBuilder.build().toBuffer().toString('hex'); ReturnClass.setResult(raw); } else { ReturnClass.setResult(fee); } ReturnClass.setFee(fee); return ReturnClass.end(); } private _getSignUTXO(utxo: any, amount: string) { let utxo_amount = 0; let utxo_list = []; const amount_total = Number(amount); const utxo_max = 600; // 循环写入数据 for (let i = 0, len = utxo.length; i < len; i++) { // 判断使用utxo是否大于交易金额 if (utxo_amount >= amount_total) continue; if (i > utxo_max) { let result: any = error.amountTooLarge; result.amount = utils.tools.toAmount(utxo_amount); return result; } utxo_amount = utils.math.plus(utxo_amount, utxo[i].amount); utxo_list.push(utxo[i]); } return {code: 0, rs: utxo_list, amount: utxo_amount}; } async signEvm(privateKey: string, amount: string, params?: IQitmeerSignEvmParams | undefined): Promise> { const ReturnClass = new Return(); params = params || {}; if (!this.verify.privateKey(privateKey)) return error.privateKey; const walletData = this.fromPrivateKeyToWallet(privateKey); const _network = qitmeer.networks[this._params.network]; let pkAddress; if (params.toPublicKey && params.toPublicKey.indexOf('0x') === 0) { pkAddress = this.getPKAddress(params.toPublicKey.substring(2, params.toPublicKey.length)); } else { pkAddress = this.getPKAddress(walletData.publicKey); } const getUTXO = await this.api.getUTXO(walletData.address, undefined, amount, '1', 'reserveUsable'); const utxo = getUTXO.rs ?? {}; if (!utxo.outs || utxo.outs.length === 0) return error.amountNotEnough; // 初始化签名 const TxBuilder = new qitmeer.txsign.newSigner(_network); TxBuilder.setTimestamp(Math.ceil(new Date().getTime() / 1e3)); const keyPair = this.getKeyPair(privateKey); // 循环写入数据 let utxoList = []; // 获取手续费 const fee = this.getFee(utxo.outs.length); // 验证余额手续费 const verifyBalance = await this.verifyBalance(walletData.address, amount, {fee}, true); if (verifyBalance.code > 0) return verifyBalance; const amountTotal = utils.math.plus(amount, fee); const utxoData = this._getSignUTXO(utxo.outs, amountTotal.toString()); if (utxoData.code > 0) return utxoData; utxo.amount = utxoData.amount; utxoList = utxoList.concat(utxoData.rs); for (let i = 0, len = utxoList.length; i < len; i++) { // 锁定 let options; if (utxoList[i].lockheight > 0) options = { prevOutType: qitmeer.script.Output.CLTV, //锁定脚本 lockTime: utxoList[i].lockheight, //锁定高度 sequence: qitmeer.tx.DEFAULT_SEQUENCE - 1, //锁定***** } TxBuilder.addInput(utxoList[i].txid, utxoList[i].number, options); } // 添加 addOutput const amountBack = utils.math.minus(utxo.amount, amountTotal); const amountInit = utils.math.multipliedBy(amount, 1e8); const amountBackDecimals = utils.math.multipliedBy(amountBack, 1e8); // console.log('addOutput', pkAddress, amountInit); // console.log('addOutput', wallet.address, amountBackDecimals); TxBuilder.addOutput(pkAddress, amountInit, 1) if (amountBackDecimals > 0) TxBuilder.addOutput(walletData.address, amountBackDecimals, 0); for (let i = 0, len = utxoList.length; i < len; i++) { TxBuilder.sign(i, keyPair); } const raw = TxBuilder.build().toHex(); ReturnClass.setResult(raw); ReturnClass.setFee(fee); return ReturnClass.end(); } async transfer(from: string, to: string, amount: string, params: ISignParams | undefined): Promise> { if (!this.verify.privateKey(params.privateKey)) return error.privateKey; const data = await this.sign(from, to, amount, params); if (data.code) return data; return await this.sendTx(data.rs); } readonly api: IApi = { getBalance: (address: string, contract: string | undefined): Promise => { contract = contract ?? 'MEER'; const url = `${this._params.api}/api/v1/status/address?address={address}&coin={contract}`; return new utils.Http().get(url, {address, contract}); }, getCoinIdList: (): Promise => { const url = `${this._params.api}/api/v1/coin`; return new utils.Http().get(url); }, getLockList: (address: string, size: number, page: number): Promise => { const url = `${this._params.api}/api/v2/transaction/lock?address={address}&page={page}&size={size}`; return new utils.Http().get(url, {address, size, page}); }, getTxDetails: (hash: string, address?: string): Promise => { const url = `${this._params.api}/api/v2/transaction/detail?txid={hash}&address={address}`; return new utils.Http().get(url, {address, hash}); }, getTxList: (type: string, address: string, contract: string | undefined, size: number, page: number): Promise => { type = type === 'all' ? '' : `/${type}`; contract = contract ?? 'MEER'; const url = `${this._params.api}/api/v2/transaction${type}?address={address}&coin={contract}&page={page}&size={size}`; return new utils.Http().get(url, {address, contract, size, page}); }, getUTXO: (address: string, contract: string, amount: string, fee: string, type?: string | undefined): Promise => { contract = contract ?? 'MEER'; const url = `${this._params.api}/api/v1/utxo?address={address}&coin={contract}&amount={amount}&fees={fee}&type={type}`; return new utils.Http().get(url, {address, contract, amount, fee, type}); }, sendTx: (raw: string, address: string | undefined, note: string | undefined): Promise => { const url = `${this._params.api}/api/v1/transaction`; return new utils.Http().post(url, {rawtx: raw, address, note}); } }; getContract(): any { return undefined } } export { Qitmeer } export type { IQitmeer }