import { DefaultAsset, MinGasLimit } from './Constant' import { ChainRpcProvider } from './providers/ChainRpcProvider' import { Setting } from './Setting' import { Transaction } from './transaction/Transaction' import * as txHelper from './utils/TxHelper' import { to } from 'await-to-js' /** * Fee structure, which contains asset amount and asset type. */ export interface Fee { /** * Amount of the fee calculated in Satoshi. */ amount: number /** * Asset type of the fee (eg:000000000000000000000000). */ asset: string } /** * Parameters used to generate a raw transaction. */ export interface GenerateRawTransactionParams { /** * From address of the transaction (eg:0x66b473d88f834b9022e0dfffd1591bde03069c71f5). */ from: string /** * To address of transaction (eg:0x63b473d88f834b9022e0dfffd1591bde03069c71f5). */ to: string /** * Amount of asset to transfer, which is calculated in satoshi. */ amount: number /** * Asset type (eg:000000000000000000000000). */ asset: string /** * Transaction fee */ fee: Fee /** * Gas of transaction which is used to run code in virtual machine. If not set, it uses the result of gas estimation provided by node rpc. */ gasLimit ? : number /** * Contract invocation type of the transaction (eg:call,template,create,deploy,vote). */ contractType ? : string /** * Hex string of code which runs in virtual machine (eg:0000000123001230000123). */ data ? : string } /** * Parameters used to generate a vote transaction. */ export interface GenerateVoteTransactionParams { /** * From address of the transaction (eg:0x66b473d88f834b9022e0dfffd1591bde03069c71f5). */ from: string /** * To address of transaction (eg:0x63b473d88f834b9022e0dfffd1591bde03069c71f5). */ to: string /** * Amount of asset used to vote, set to 0 if you want to vote all for a given asset type. */ amount: number /** * Asset type (eg:000000000000000000000000). */ asset: string /** * Vote id. */ voteId: number /** * Amount of asset used to vote, set to 0 if you want to vote all for a given asset type. */ voteValue: number /** * Transaction fee. */ fee: Fee /** * Gas of transaction which is used to run code in virtual machine. If not set, it uses the result of gas estimation provided by node rpc. */ gasLimit ? : number /** * Contract invocation type of the transaction, in vote transaction it is set to "vote". */ contractType ? : string /** * Hex string of code which runs in virtual machine (eg:0000000123001230000123). */ data ? : string } /** * Parameters used to send a transaction. */ export interface SendParams { /** * To address of transaction (eg:0x66b473d88f834b9022e0dfffd1591bde03069c71f5). */ address: string /** * Amount of asset to transfer, which is calculated in satoshi. */ amount: number /** * Asset type (eg:000000000000000000000000). */ asset: string /** * Value of transaction fee, calculated in satoshi. */ feeValue: number /** * Asset type of transaction fee (eg:000000000000000000000000). */ feeType: string } /** * High level Transactions object to send transactions on Asimov blockchain */ export class Transactions { public setting: Setting = Setting.getInstance() private _privateKey: string = this.setting.privateKey private _chainRpcProvider: ChainRpcProvider = this.setting.chainRpcProvider constructor(rpc: ChainRpcProvider) { if (rpc) { this.chainRpcProvider = rpc } } public get chainRpcProvider(): ChainRpcProvider { return this._chainRpcProvider } public set chainRpcProvider(rpc: ChainRpcProvider) { this._chainRpcProvider = rpc } public get privateKey(): string { return this._privateKey } public set privateKey(pk: string) { this._privateKey = pk } private async pickUtxos(amount: number, asset: string, address: string, page: number) { let inputs: any[] = [] let total = 0 let pageCount = 1000 let [err, res] = await to(this.chainRpcProvider.getUtxoInPage({ address: address, asset: asset, from: page, count: pageCount })) if (err) { throw err } let { utxos, count } = res for (let i = 0, len = utxos.length; i < len; i++) { let u = utxos[i]; if (amount == 0) { total += u.amount inputs.push(u) break } if (total < amount) { total += u.amount inputs.push(u) } } //TODO bignumber if (total < amount && inputs.length < count) { //TODO bignumber let [err, res] = await to(this.pickUtxos(amount - total, asset, address, page + 1)) if (err) { throw err; } inputs = inputs.concat(res) } return inputs } private async pickVoteUtxos(voteValue: number, asset: string, address: string) { let inputs: any[] = [] let page = 0 let total = 0 let pageCount = 1000 let [err, res] = await to(this.chainRpcProvider.getUtxoInPage({ address: address, asset: asset, from: page, count: pageCount })) if (err) { throw err } let { utxos, count } = res let [err1, res1] = await to(this.chainRpcProvider.getUtxoInPage({ address: address, asset: asset, from: 0, count: count })) if (err1) { throw err1 } inputs = res1.utxos return inputs } private async pickFeeUtxos(amount: number, asset: string, address: string) { let total = 0 let feeInputs = await this.pickUtxos(amount, asset, address, 0) //let [err, feeInputs] = await to(temp) if (!feeInputs.length) { throw new Error("No enough utxo to contruct transaction as fee") } feeInputs.forEach(i => { total += i.amount }) if (total < amount) { throw new Error("No enough utxo to contruct transaction as fee") } return feeInputs } private async estimateFee(amount: number, asset: string, address: string, outputs: any[], gasLimit: number) { let inputs = await this.pickUtxos(amount, asset, address, 0) let feeValue = txHelper.estimateFee(inputs, outputs, gasLimit) let feeType = DefaultAsset return { amount: feeValue, asset: feeType } } /** * Construct raw transaction object. * @param params Parameters to construct a raw transaction object. * @return Transaction id */ public async generateRawTransaction(params: GenerateRawTransactionParams) { let { from, to, amount, asset = DefaultAsset, fee, gasLimit = MinGasLimit, contractType, data } = params if (!from) { throw new Error("From address is not specified") } if (!to) { throw new Error("Destination address is not specified") } let total = 0 let totalAmount = amount let changeMap: { [propName: string]: number } = {} let outputs: any[] = [{ data: data, assets: asset, amount: amount, address: to, contractType: contractType }] //estimate fee if fee is not sepecified if (!fee || !fee.amount) { fee = await this.estimateFee(amount, asset, from, outputs, gasLimit) } if (fee.asset == asset) { //TODO bignumber totalAmount = totalAmount + fee.amount } let inputs = await this.pickUtxos(totalAmount, asset, from, 0) if (!inputs.length) { throw new Error("No enough utxo to contruct transaction as inputs") } inputs.forEach(i => { total += i.amount }) if (total < totalAmount) { throw new Error("No enough utxo to contruct transaction") } //Fee utxos if (fee.asset !== asset && fee.amount) { let feeInputs = await this.pickFeeUtxos(fee.amount, fee.asset, from) inputs = inputs.concat(feeInputs) } //TODO bignumber inputs.forEach(i => { let amount = i.amount i.amount = amount.toString() if (changeMap[i.assets]) { changeMap[i.assets] += amount } else { changeMap[i.assets] = amount } }) //TODO bignumber changeMap[asset] -= amount if (fee.amount) { changeMap[fee.asset] -= fee.amount } //TODO bignumber for (let k in changeMap) { let v = changeMap[k] outputs.push({ assets: k, amount: v.toString(), address: from }) } let tx = new Transaction({ inputs: inputs, outputs: outputs, gasLimit: gasLimit }) return tx } /** * Construct vote transaction object. * @param params Parameters to construct a vote transaction object. * @return Transaction id */ public async generateVoteTransaction(params: GenerateVoteTransactionParams) { let { from, to, asset = DefaultAsset, fee, gasLimit = MinGasLimit, contractType, data, amount, voteValue, voteId } = params let total = 0 let changeMap: { [propName: string]: number } = {} let outputs: any[] = [{ data: data, assets: asset, amount: 0, address: to, contractType: contractType }] if (!from) { throw new Error("From address is not specified") } if (!to) { throw new Error("Destination address is not specified") } let inputs: any[] = [] inputs = await this.pickVoteUtxos(voteValue, asset, from) let totalVote = 0 let temp: any[] = [] inputs.forEach(i => { let noVote = false if (i.locks) { i.locks.forEach(lock => { let { lockAddress, id } = txHelper.parseLockId(lock.id) if (lockAddress == to && id == voteId) { if (lock.amount < i.amount) { totalVote += i.amount - lock.amount temp.push(i) } else { noVote = true } } }) } if (!noVote) { totalVote += i.amount temp.push(i) } }) inputs = temp if (!inputs.length) { throw new Error("No enough utxo to vote") } //estimate fee if fee is not sepecified if (!fee || !fee.amount) { fee = await this.estimateFee(amount, asset, from, outputs, gasLimit) } if (fee.asset !== asset && fee.amount) { let feeInputs = await this.pickFeeUtxos(fee.amount, fee.asset, from) inputs = inputs.concat(feeInputs) } else if (totalVote <= fee.amount) { throw new Error("No enough utxo as fee to vote") } //TODO bignumber inputs.forEach(i => { if (changeMap[i.assets]) { changeMap[i.assets] += i.amount } else { changeMap[i.assets] = i.amount } i.amount = i.amount.toString() }) if (changeMap[fee.asset] < fee.amount) { throw new Error("Not enough balance to pay the transaction fee") } //TODO bignumber if (fee.amount) { changeMap[fee.asset] -= fee.amount } //TODO bignumber for (let k in changeMap) { let v = changeMap[k] outputs.push({ assets: k, amount: v.toString(), address: from }) } let tx = new Transaction({ inputs: inputs, outputs: outputs, gasLimit: gasLimit }) return tx } /** * Construct a normal transaction and send it on Asimov blockchain. * @param params Parameters to send a transaction on Asimov blockchain. * @return Transaction id */ public async send(params: SendParams) { let { address, amount, asset = DefaultAsset, feeValue, feeType } = params if (!amount) { throw new Error("Can not transfer asset with 0 amount to " + address) } let fee = this.setting.fee if (feeValue && feeType) { fee = { amount: feeValue, asset: feeType } } let from = txHelper.getAddressByPrivateKey(this.privateKey) let txParams: GenerateRawTransactionParams = { from: from, to: address, amount: amount, asset: asset, fee: fee } let [err1, tx] = await to(this.generateRawTransaction(txParams)) if (err1) { throw err1 } let rawTx = tx.sign([this.privateKey]).toHex() let [err2, res] = await to(this.chainRpcProvider.sendRawTransaction(rawTx)) if (err2) { throw err2 } return res } /** * Check whether a transaction is confirmed on chain. * @param txId transaction id. * @return true or false */ public async check(txId: string) { let [err, res] = await to(this.chainRpcProvider.getRawTransaction({ txId: txId, verbose: true })) if (err) { throw err } let { confirmations } = res if (confirmations > 0) { return true } else { return false } } /** * Fetch transaction details. * @param txId transaction id. * @return Transaction details. */ public async fetch(txId: string) { let [err, res] = await to(this.chainRpcProvider.getTransactionReceipt(txId)) if (err) { throw err } return res } }