UNPKG

@kazeblockchain/kazejs

Version:

Javascript libraries for Kaze wallet

165 lines (156 loc) 6.86 kB
import { num2VarInt, num2hexstring, StringStream, reverseHex, hash256, Fixed8 } from '../utils' import { Account, AssetBalance, generateSignature, getVerificationScriptFromPublicKey, getPublicKeyFromPrivateKey, getScriptHashFromAddress, isPrivateKey } from '../wallet' import { serializeExclusive, deserializeExclusive } from './exclusive' import { ASSETS, ASSET_ID } from '../consts' import * as comp from './components' import { defaultCalculationStrategy } from '../settings' import logger from '../logging' const log = logger('tx') /** * Calculate the inputs required given the intents and STREAMCost. STREAMCost has to be seperate because it will not be reflected as an TransactionOutput. * @param {Balance} balances - Balance of all assets available. * @param {TransactionOutput[]} intents - All sending intents * @param {number|Fixed8} extraCost - STREAMCost required for the transaction. * @param {function} strategy * @param {number|Fixed8} fees * @return {object} {inputs: TransactionInput[], change: TransactionOutput[] } */ export const calculateInputs = (balances, intents, extraCost = 0, strategy = null, fees = 0) => { if (intents === null) intents = [] if (strategy === null) strategy = defaultCalculationStrategy const requiredAssets = intents.reduce((assets, intent) => { assets[intent.assetId] ? assets[intent.assetId] = assets[intent.assetId].add(intent.value) : assets[intent.assetId] = intent.value return assets }, {}) // Add STREAM cost and fees in extraCost = new Fixed8(extraCost).add(fees) if (extraCost.gt(0)) { if (requiredAssets[ASSET_ID.STREAM]) { requiredAssets[ASSET_ID.STREAM] = requiredAssets[ASSET_ID.STREAM].add(extraCost) } else { requiredAssets[ASSET_ID.STREAM] = extraCost } } const inputsAndChange = Object.keys(requiredAssets).map((assetId) => { const requiredAmt = requiredAssets[assetId] const assetSymbol = ASSETS[assetId] if (balances.assetSymbols.indexOf(assetSymbol) === -1) throw new Error(`This balance does not contain any ${assetSymbol}!`) const assetBalance = balances.assets[assetSymbol] if (assetBalance.balance.lt(requiredAmt)) throw new Error(`Insufficient ${ASSETS[assetId]}! Need ${requiredAmt.toString()} but only found ${assetBalance.balance.toString()}`) return calculateInputsForAsset(AssetBalance(assetBalance), requiredAmt, assetId, balances.address, strategy) }) const output = inputsAndChange.reduce((prev, curr) => { return { inputs: prev.inputs.concat(curr.inputs), change: prev.change.concat(curr.change) } }, { inputs: [], change: [] }) return output } const calculateInputsForAsset = (assetBalance, requiredAmt, assetId, address, strategy) => { const selectedInputs = strategy(assetBalance, requiredAmt) const selectedAmt = selectedInputs.reduce((prev, curr) => prev.add(curr.value), new Fixed8(0)) const change = [] // Construct change output if (selectedAmt.gt(requiredAmt)) { change.push({ assetId, value: selectedAmt.sub(requiredAmt), scriptHash: getScriptHashFromAddress(address) }) } // Format inputs const inputs = selectedInputs.map((input) => { return { prevHash: input.txid, prevIndex: input.index } }) return { inputs, change } } /** * Serializes a given transaction object * @param {Transaction} tx * @param {boolean} signed - If the signatures should be serialized * @returns {string} Hexstring of transaction */ export const serializeTransaction = (tx, signed = true) => { let out = '' out += num2hexstring(tx.type) out += num2hexstring(tx.version) out += serializeExclusive[tx.type](tx) out += num2VarInt(tx.attributes.length) for (const attribute of tx.attributes) { out += comp.serializeTransactionAttribute(attribute) } out += num2VarInt(tx.inputs.length) for (const input of tx.inputs) { out += comp.serializeTransactionInput(input) } out += num2VarInt(tx.outputs.length) for (const output of tx.outputs) { out += comp.serializeTransactionOutput(output) } if (signed && tx.scripts && tx.scripts.length > 0) { out += num2VarInt(tx.scripts.length) for (const script of tx.scripts) { out += comp.serializeWitness(script) } } return out } /** * Deserializes a given string into a Transaction object * @param {string} data - Serialized string * @returns {Transaction} Transaction object */ export const deserializeTransaction = (data) => { const ss = new StringStream(data) let tx = {} tx.type = parseInt(ss.read(1), 16) tx.version = parseInt(ss.read(1), 16) const exclusiveData = deserializeExclusive[tx.type](ss) tx.attributes = [] tx.inputs = [] tx.outputs = [] tx.scripts = [] const attrLength = ss.readVarInt() for (let i = 0; i < attrLength; i++) { tx.attributes.push(comp.deserializeTransactionAttribute(ss)) } const inputLength = ss.readVarInt() for (let i = 0; i < inputLength; i++) { tx.inputs.push(comp.deserializeTransactionInput(ss)) } const outputLength = ss.readVarInt() for (let i = 0; i < outputLength; i++) { tx.outputs.push(comp.deserializeTransactionOutput(ss)) } if (!ss.isEmpty()) { const scriptLength = ss.readVarInt() for (let i = 0; i < scriptLength; i++) { tx.scripts.push(comp.deserializeWitness(ss)) } } return Object.assign(tx, exclusiveData) } /** * Signs a transaction with the corresponding privateKey. We are dealing with it as an Transaction object as multi-sig transactions require us to sign the transaction without signatures. * @param {Transaction} transaction - Transaction as an object * @param {string} privateKey - The private key. This method does not check if the private key is valid (aka that the inputs come from the corresponding address) * @return {Transaction} Signed transaction as an object. */ export const signTransaction = (transaction, privateKey) => { if (!isPrivateKey(privateKey)) throw new Error('Key provided does not look like a private key!') const acct = new Account(privateKey) const invocationScript = '40' + generateSignature(serializeTransaction(transaction, false), privateKey) const verificationScript = getVerificationScriptFromPublicKey(getPublicKeyFromPrivateKey(privateKey)) const witness = { invocationScript, verificationScript } transaction.scripts ? transaction.scripts.push(witness) : transaction.scripts = [witness] log.info(`Signed tx ${transaction.hash} with Account[${acct.address}]`) return transaction } /** * @param {Transaction} transaction * @return {string} */ export const getTransactionHash = (transaction) => { return reverseHex(hash256(serializeTransaction(transaction, false))) }