Skip to content

Commit

Permalink
feat: fee estimation for claim and refund transactions (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
michael1011 authored and peartobear committed Nov 27, 2018
1 parent 253544d commit 75afad8
Show file tree
Hide file tree
Showing 16 changed files with 204 additions and 89 deletions.
8 changes: 4 additions & 4 deletions lib/chain/ChainClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,16 +94,16 @@ class ChainClient extends BaseClient implements ChainClientInterface {
}

/**
* Returns the estimated fee in sats per kilobyte
* Returns the estimated fee in stoshis per byte
*
* @param blocks after how many blocks the transaction should confirm
*/
public estimateFee = async (blocks: number): Promise<number> => {
// BTCD returns the amount of Bitcoins not satoshis and therefore the returned amount
// has to be multipled by 100 million to get the amount of satohis per kilobyte
// BTCD returns the amount in whole Bitcoins per kilobyte not satoshis per byte and therefore the
// returned amount has to be multipled by 100000 to get the amount of satohis per byte
const bitcoins = await this.rpcClient.call<number>('estimatefee', blocks);

return Math.ceil(bitcoins * 100000000);
return Math.ceil(bitcoins * 100000);
}

public sendRawTransaction = (rawTransaction: string, allowHighFees = true): Promise<string> => {
Expand Down
7 changes: 6 additions & 1 deletion lib/cli/BuilderComponents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default {
default: 'compatibility',
},
network: {
describe: 'network on which the claim transaction should be broadcasted',
describe: 'network on which the transaction will be used',
type: 'string',
},
lockupTransaction: {
Expand All @@ -30,4 +30,9 @@ export default {
describe: 'address to which the claimed funds should be sent',
type: 'string',
},
feePerByte: {
describe: 'amount of satoshis per vbyte that should be paid as fee',
type: 'number',
detauls: '1',
},
};
13 changes: 8 additions & 5 deletions lib/cli/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,17 @@ export const claimSwap = (argv: Arguments) => {
const swapOutput = parseSwapOutput(redeemScript, lockupTransaction);

const claimTransaction = constructClaimTransaction(
getHexBuffer(argv.preimage),
claimKeys,
destinationScript,
{
redeemScript,
preimage: getHexBuffer(argv.preimage),
keys: claimKeys,
},
{
txHash: lockupTransaction.getHash(),
...swapOutput,
},
redeemScript,
destinationScript,
argv.fee_per_byte,
);

return claimTransaction.toHex();
Expand All @@ -83,9 +86,9 @@ export const refundSwap = (argv: Arguments) => {
const swapOutput = parseSwapOutput(redeemScript, lockupTransaction);

const refundTransaction = constructRefundTransaction(
argv.timeout_block_height,
refundKeys,
destinationScript,
argv.timeout_block_height,
{
txHash: lockupTransaction.getHash(),
...swapOutput,
Expand Down
3 changes: 2 additions & 1 deletion lib/cli/commands/ClaimSwap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { claimSwap } from '../Utils';
import { printResponse } from '../Command';
import BuilderComponents from '../BuilderComponents';

export const command = 'claimswap <network> <lockup_transaction> <redeem_script> <preimage> <claim_private_key> <destination_address>';
export const command = 'claimswap <network> <lockup_transaction> <redeem_script> <preimage> <claim_private_key> <destination_address> [fee_per_byte]';

export const describe = 'claims the onchain part of a reverse swap';

Expand All @@ -20,6 +20,7 @@ export const builder = {
type: 'string',
},
destination_address: BuilderComponents.destinationAddress,
fee_per_byte: BuilderComponents.feePerByte,
};

export const handler = (argv: Arguments) => {
Expand Down
6 changes: 4 additions & 2 deletions lib/cli/commands/RefundSwap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { refundSwap } from '../Utils';
import { printResponse } from '../Command';
import BuilderComponents from '../BuilderComponents';

export const command = 'refundswap <network> <lockup_transaction> <redeem_script> <timeout_block_height> <refund_private_key> <destination_address>';
export const command = 'refundswap <network> <lockup_transaction> <redeem_script> <timeout_block_height> <refund_private_key> ' +
'<destination_address> [fee_per_byte]';

export const describe = 'refunds the onchain part of a swap';

Expand All @@ -12,14 +13,15 @@ export const builder = {
lockup_transaction: BuilderComponents.lockupTransaction,
redeem_script: BuilderComponents.redeemScript,
timeout_block_height: {
describe: 'timeout block height of the CTLV',
describe: 'timeout block height of the timelock',
type: 'number',
},
refund_private_key: {
describe: 'public key with which a refund transaction has to be signed',
type: 'string',
},
destination_address: BuilderComponents.destinationAddress,
fee_per_byte: BuilderComponents.feePerByte,
};

export const handler = (argv: Arguments) => {
Expand Down
40 changes: 26 additions & 14 deletions lib/swap/Claim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,47 +7,59 @@ import ops from '@michael1011/bitcoin-ops';
import * as bip65 from 'bip65';
import * as varuint from 'varuint-bitcoin';
import { Transaction, crypto, script, ECPair } from 'bitcoinjs-lib';
import { encodeSignature, scriptBuffersToScript } from './SwapUtils';
import { encodeSignature, scriptBuffersToScript, getOutputScriptType } from './SwapUtils';
import { estimateFee } from '../wallet/FeeCalculator';
import { OutputType } from '../proto/boltzrpc_pb';
import { TransactionOutput } from '../consts/Types';

export type ClaimDetails = {
preimage: Buffer;
keys: ECPair | BIP32;
redeemScript: Buffer;
};

// TODO: claiming with multiple UTXOs
// TODO: support for RBF
/**
* Claim a swap
*
* @param preimage the preimage of the transaction
* @param claimKeys the key pair needed to claim the swap
* @param claimDetails preimage, key pair and redeemScript needed for claiming the swap
* @param utxo amount of satoshis per vbyte that should be paid as fee
* @param destinationScript the output script to which the funds should be sent
* @param utxo the swap UTXO to claim
* @param redeemScript the redeem script of the swap
* @param feePerByte how many satoshis per vbyte should be paid as fee
* @param timeoutBlockHeight locktime of the transaction; only needed if used used for a refund
*
* @returns claim transaction
*/
export const constructClaimTransaction = (preimage: Buffer, claimKeys: ECPair | BIP32, destinationScript: Buffer, utxo: TransactionOutput,
redeemScript: Buffer, timeoutBlockHeight?: number): Transaction => {
export const constructClaimTransaction = (claimDetails: ClaimDetails, utxo: TransactionOutput,
destinationScript: Buffer, feePerByte = 1, timeoutBlockHeight?: number): Transaction => {

const { preimage, keys, redeemScript } = claimDetails;

const tx = new Transaction();

// Refund uses this method to generate refund transactions and CTLVs
// require the transaction to have a locktime after the timeout
// Refunding transactions are just like claiming ones and therefore
// this method is also used for refunds. In orders to use to use
// the timelock needed for the refund the locktime of the transaction
// has to be after the timelock is over.
if (timeoutBlockHeight) {
tx.locktime = bip65.encode({ blocks: timeoutBlockHeight });
}

// Add the swap as input to the transaction
tx.addInput(utxo.txHash, utxo.vout, 0);

// TODO: fee estimation
tx.addOutput(destinationScript, utxo.value - 1000);
// Estimate the fee for the transaction
const fee = estimateFee(feePerByte, [{ type: utxo.type, swapDetails: { preimage, redeemScript } }], [getOutputScriptType(destinationScript)!]);

// Send the swap value minues the estimated fee to the destination address
tx.addOutput(destinationScript, utxo.value - fee);

// Add missing witness and scripts
switch (utxo.type) {
// Construct the signed input scripts for P2SH inputs
case OutputType.LEGACY:
const sigHash = tx.hashForSignature(0, redeemScript, Transaction.SIGHASH_ALL);
const signature = claimKeys.sign(sigHash);
const signature = keys.sign(sigHash);

const inputScript = [
encodeSignature(Transaction.SIGHASH_ALL, signature),
Expand Down Expand Up @@ -75,7 +87,7 @@ export const constructClaimTransaction = (preimage: Buffer, claimKeys: ECPair |
// Construct the signed witness for (nested) SegWit inputs
if (utxo.type !== OutputType.LEGACY) {
const sigHash = tx.hashForWitnessV0(0, redeemScript, utxo.value, Transaction.SIGHASH_ALL);
const signature = script.signature.encode(claimKeys.sign(sigHash), Transaction.SIGHASH_ALL);
const signature = script.signature.encode(keys.sign(sigHash), Transaction.SIGHASH_ALL);

tx.setWitness(0, [
signature,
Expand Down
24 changes: 13 additions & 11 deletions lib/swap/Refund.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,30 @@ import { constructClaimTransaction } from './Claim';
const hexBase = 16;
const dummyPreimage = getHexBuffer(ops.OP_FALSE.toString(hexBase));

// TODO: add unit tests
// TODO: same TODOs as in Claim.ts
/**
* Refund a swap
*
* @param timeoutBlockHeight block height at which the swap times out
* @param refundKeys the key pair needed to refund the swap
* @param destinationScript the output script to which the funds should be sent
* @param redeemScript redeem script of the swap
* @param timeoutBlockHeight block height at which the swap times out
* @param utxo the swap UTXO to claim
* @param redeemScript the redeem script of the swap
* @param destinationScript the output script to which the funds should be sent
* @param feePerByte how many satoshis per vbyte should be paid as fee
*
* @returns refund transaction
*/
export const constructRefundTransaction = (timeoutBlockHeight: number, refundKeys: ECPair | BIP32, destinationScript: Buffer,
utxo: TransactionOutput, redeemScript: Buffer) => {
export const constructRefundTransaction = (refundKeys: ECPair | BIP32, redeemScript: Buffer, timeoutBlockHeight: number, utxo: TransactionOutput,
destinationScript: Buffer, feePerByte = 1) => {

return constructClaimTransaction(
dummyPreimage,
refundKeys,
destinationScript,
{
redeemScript,
keys: refundKeys,
preimage: dummyPreimage,
},
utxo,
redeemScript,
destinationScript,
feePerByte,
timeoutBlockHeight,
);
};
12 changes: 8 additions & 4 deletions lib/swap/SwapManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,19 +240,23 @@ class SwapManager {
this.logger.verbose(`Got preimage: ${preimage}`);

const destinationScript = p2wpkhOutput(crypto.hash160(details.claimKeys.publicKey));
const feePerByte = await currency.chainClient.estimateFee(1);

const claimTx = constructClaimTransaction(
Buffer.from(preimage, 'base64'),
details.claimKeys,
destinationScript,
{
preimage: Buffer.from(preimage, 'base64'),
keys: details.claimKeys,
redeemScript: details.redeemScript,
},
{
txHash,
vout,
type: details.outputType,
script: outpuScript,
value: outputValue,
},
details.redeemScript,
destinationScript,
feePerByte,
);

this.logger.silly(`Broadcasting claim transaction: ${claimTx.getId()}`);
Expand Down
52 changes: 46 additions & 6 deletions lib/swap/SwapUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,18 @@
*/

// TODO: add missing typings
import bip66 from 'bip66';
import { script } from 'bitcoinjs-lib';
import Bn from 'bn.js';
import bip66 from 'bip66';
import ops from '@michael1011/bitcoin-ops';
import * as varuint from 'varuint-bitcoin';
import { getHexBuffer, getHexString } from '../Utils';
import { ScriptElement } from '../consts/Types';
import { Currency } from '../wallet/WalletManager';
import { Output } from '../wallet/FeeCalculator';
import { OutputType } from '../proto/boltzrpc_pb';

const zeroHexBuffer = getHexBuffer('00');

/**
* DER encode bytes to eliminate sign confusion in a big-endian number
Expand All @@ -18,8 +24,6 @@ import { Currency } from '../wallet/WalletManager';
* @returns an encoded point buffer
*/
const derEncode = (point: string) => {
const zero = getHexBuffer('00');

let i = 0;
let x = getHexBuffer(point);

Expand All @@ -28,13 +32,13 @@ const derEncode = (point: string) => {
}

if (i === x.length) {
return zero;
return zeroHexBuffer;
}

x = x.slice(i);

if (x[0] & 0x80) {
return Buffer.concat([zero, x], x.length + 1);
return Buffer.concat([zeroHexBuffer, x], x.length + 1);
} else {
return x;
}
Expand Down Expand Up @@ -104,7 +108,43 @@ export const toPushdataScript = (elements: ScriptElement[]): Buffer => {
};

/**
* Gets the BIP21 prefix for a currency
* Get the OutputType and whether it is a SH of a output script
*/
export const getOutputScriptType = (outputScript: Buffer): Output | undefined => {
const rawScript = script.decompile(outputScript);

if (rawScript.length > 1) {
switch (rawScript[0]) {
case ops.OP_0:
// If the second entry of the script array has the length of 20 it is a
// PKH output if not it is a SH output
const secondEntry = rawScript[1] as Buffer;
let isSh = false;

if (secondEntry.length !== 20) {
isSh = true;
}

return {
isSh,
type: OutputType.BECH32,
};

case ops.OP_HASH160:
// The FeeCalculator treats legacy SH outputs the same way as compatibility PKH ones
// Which one of the aforementioned types the outputScript is does not
// matter for the fee estimation of a output
return { type: OutputType.LEGACY, isSh: true };

case ops.OP_DUP: return { type: OutputType.LEGACY, isSh: false };
}
}

return;
};

/**
* Get the BIP21 prefix for a currency
*/
export const getBip21Prefix = (currency: Currency) => {
return currency.symbol === 'BTC' ? 'bitcoin' : 'litecoin';
Expand Down
4 changes: 2 additions & 2 deletions lib/wallet/FeeCalculator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { OutputType } from '../proto/boltzrpc_pb';

type Input = {
export type Input = {
type: OutputType,

// In case the input is a Swap this fields have to be set
Expand All @@ -10,7 +10,7 @@ type Input = {
};
};

type Output = {
export type Output = {
type: OutputType;

isSh?: boolean;
Expand Down
4 changes: 2 additions & 2 deletions lib/wallet/Wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ class Wallet {
Promise<{ tx: Transaction, vout: number }> => {

const utxos = await this.utxoRepository.getUtxosSorted(this.symbol);
const feePerByte = Math.ceil(await this.chainClient.estimateFee(1) / 1000);
const feePerByte = await this.chainClient.estimateFee(1);

// The UTXOs that will be spent
const toSpend: UTXO[] = [];
Expand All @@ -245,7 +245,7 @@ class Wallet {
return (amount + fee) <= toSpendSum;
};

// Accumulate UTXO to spend
// Accumulate UTXOs to spend
for (const utxoInstance of utxos) {
const redeemScript = utxoInstance.redeemScript ? getHexBuffer(utxoInstance.redeemScript) : undefined;

Expand Down
Loading

0 comments on commit 75afad8

Please sign in to comment.