blockstack
Version:
The Blockstack Javascript library for authentication, identity, and storage.
228 lines (197 loc) • 7 kB
text/typescript
import { TransactionBuilder, Transaction, TxOutput, crypto as bjsCrypto } from 'bitcoinjs-lib'
import RIPEMD160 from 'ripemd160'
import BN from 'bn.js'
import { NotEnoughFundsError } from '../errors'
import { TransactionSigner } from './signers'
import { UTXO } from '../network'
/**
*
* @ignore
*/
export const DUST_MINIMUM = 5500
/**
*
* @ignore
*/
export function hash160(buff: Buffer) {
const sha256 = bjsCrypto.sha256(buff)
return (new RIPEMD160()).update(sha256).digest()
}
/**
*
* @ignore
*/
export function hash128(buff: Buffer) {
return Buffer.from(bjsCrypto.sha256(buff).slice(0, 16))
}
// COPIED FROM coinselect, because 1 byte matters sometimes.
// baseline estimates, used to improve performance
const TX_EMPTY_SIZE = 4 + 1 + 1 + 4
const TX_INPUT_BASE = 32 + 4 + 1 + 4
const TX_INPUT_PUBKEYHASH = 107
const TX_OUTPUT_BASE = 8 + 1
const TX_OUTPUT_PUBKEYHASH = 25
type txPoint = {
script: { length: number }
}
function inputBytes(input: txPoint | null) {
if (input && input.script && input.script.length > 0) {
return TX_INPUT_BASE + input.script.length
} else {
return TX_INPUT_BASE + TX_INPUT_PUBKEYHASH
}
}
function outputBytes(output: txPoint | null) {
if (output && output.script && output.script.length > 0) {
return TX_OUTPUT_BASE + output.script.length
} else {
return TX_OUTPUT_BASE + TX_OUTPUT_PUBKEYHASH
}
}
function transactionBytes(inputs: Array<txPoint | null>, outputs: Array<txPoint | null>) {
return TX_EMPTY_SIZE
+ inputs.reduce((a: number, x: txPoint | null) => (a + inputBytes(x)), 0)
+ outputs.reduce((a: number, x: txPoint | null) => (a + outputBytes(x)), 0)
}
/**
*
* @ignore
*/
export function getTransactionInsideBuilder(txBuilder: TransactionBuilder) {
return ((txBuilder as any).__TX as Transaction)
}
function getTransaction(txIn: Transaction | TransactionBuilder) {
if (txIn instanceof Transaction) {
return txIn
}
return getTransactionInsideBuilder(txIn)
}
//
/**
*
* @ignore
*/
export function estimateTXBytes(txIn: Transaction | TransactionBuilder,
additionalInputs: number,
additionalOutputs: number) {
const innerTx = getTransaction(txIn)
const dummyInputs: Array<null> = new Array(additionalInputs)
dummyInputs.fill(null)
const dummyOutputs: Array<null> = new Array(additionalOutputs)
dummyOutputs.fill(null)
const inputs: Array<null | txPoint> = [].concat(innerTx.ins, dummyInputs)
const outputs: Array<null | txPoint> = [].concat(innerTx.outs, dummyOutputs)
return transactionBytes(inputs, outputs)
}
/**
*
* @ignore
*/
export function sumOutputValues(txIn: Transaction | TransactionBuilder) {
const innerTx = getTransaction(txIn)
return innerTx.outs.reduce((agg, x) => agg + (x as TxOutput).value, 0)
}
/**
*
* @ignore
*/
export function decodeB40(input: string) {
// treat input as a base40 integer, and output a hex encoding
// of that integer.
//
// for each digit of the string, find its location in `characters`
// to get the value of the digit, then multiply by 40^(-index in input)
// e.g.,
// the 'right-most' character has value: (digit-value) * 40^0
// the next character has value: (digit-value) * 40^1
//
// hence, we reverse the characters first, and use the index
// to compute the value of each digit, then sum
const characters = '0123456789abcdefghijklmnopqrstuvwxyz-_.+'
const base = new BN(40)
const inputDigits = input.split('').reverse()
const digitValues = inputDigits.map(
((character: string, exponent: number) => new BN(characters.indexOf(character))
.mul(base.pow(new BN(exponent))))
)
const sum = digitValues.reduce(
(agg: BN, cur: BN) => agg.add(cur),
new BN(0)
)
return sum.toString(16, 2)
}
/**
* Adds UTXOs to fund a transaction
* @param {TransactionBuilder} txBuilderIn - a transaction builder object to add the inputs to. this
* object is _always_ mutated. If not enough UTXOs exist to fund, the tx builder object
* will still contain as many inputs as could be found.
* @param {Array<{value: number, tx_hash: string, tx_output_n}>} utxos - the utxo set for the
* payer's address.
* @param {number} amountToFund - the amount of satoshis to fund in the transaction. the payer's
* utxos will be included to fund up to this amount of *output* and the corresponding *fees*
* for those additional inputs
* @param {number} feeRate - the satoshis/byte fee rate to use for fee calculation
* @param {boolean} fundNewFees - if true, this function will fund `amountToFund` and any new fees
* associated with including the new inputs.
* if false, this function will fund _at most_ `amountToFund`
* @returns {number} - the amount of leftover change (in satoshis)
* @private
* @ignore
*/
export function addUTXOsToFund(txBuilderIn: TransactionBuilder,
utxos: Array<UTXO>,
amountToFund: number, feeRate: number,
fundNewFees: boolean = true): number {
if (utxos.length === 0) {
throw new NotEnoughFundsError(amountToFund)
}
// how much are we increasing fees by adding an input ?
const newFees = feeRate * (estimateTXBytes(txBuilderIn, 1, 0)
- estimateTXBytes(txBuilderIn, 0, 0))
let utxoThreshhold = amountToFund
if (fundNewFees) {
utxoThreshhold += newFees
}
const goodUtxos = utxos.filter(utxo => utxo.value >= utxoThreshhold)
if (goodUtxos.length > 0) {
goodUtxos.sort((a, b) => a.value - b.value)
const selected = goodUtxos[0]
let change = selected.value - amountToFund
if (fundNewFees) {
change -= newFees
}
txBuilderIn.addInput(selected.tx_hash, selected.tx_output_n)
return change
} else {
utxos.sort((a, b) => b.value - a.value)
const largest = utxos[0]
if (newFees >= largest.value) {
throw new NotEnoughFundsError(amountToFund)
}
txBuilderIn.addInput(largest.tx_hash, largest.tx_output_n)
let remainToFund = amountToFund - largest.value
if (fundNewFees) {
remainToFund += newFees
}
return addUTXOsToFund(txBuilderIn, utxos.slice(1),
remainToFund, feeRate, fundNewFees)
}
}
export function signInputs(txB: TransactionBuilder,
defaultSigner: TransactionSigner,
otherSigners?: Array<{index: number, signer: TransactionSigner}>) {
const txInner = getTransactionInsideBuilder(txB)
const signerArray = txInner.ins.map(() => defaultSigner)
if (otherSigners) {
otherSigners.forEach((signerPair) => {
signerArray[signerPair.index] = signerPair.signer
})
}
let signingPromise = Promise.resolve()
for (let i = 0; i < txInner.ins.length; i++) {
signingPromise = signingPromise.then(
() => signerArray[i].signTransaction(txB, i)
)
}
return signingPromise.then(() => txB)
}