# Quick start
In this article we will show you how to send bitcoin as cheaply as possible, to be confirmed by a date/time of your choosing, instead of choosing a fee. If you already have some bitcoin in hand, you can try it yourself. It takes less than 10 minutes - if you have any issues, feel free to get in touch.
The code in this tutorial can also be found in this github repository.
# Setting up your environment
If you haven't python installed, install it. We will use the user-friendly python library bit along with a wrapper for the bitpost interface, to make things easy. Install the dependencies with, pip install bit bitpost numpy
.
We will use the popular library bitcoinJS and coinselect (from the same team) for creating and signing transactions. Install the dependencies with npm i bitpost bitcoinjs-lib coinselect bip39
.
We will use the popular library bitcoinJS and coinselect (from the same team) for creating and signing transactions. Install the dependencies with npm i bitpost bitcoinjs-lib coinselect bip39
.
We will use two packages that need to be installed with pub get
:
- bitcoin flutter (
import 'package:bitcoin_flutter/bitcoin_flutter.dart';
): allows us to build and sign transactions. - bitpost (
import 'package:bitpost/bitpost.dart';
): wraps the bitpost API and simplifies the communication.
We will use bitcoinJ as our bitcoin library. If you are using maven, add the following dependencies to your pom.xml
:
<dependencies>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>com.github.bitcoinj</groupId>
<artifactId>bitcoinj</artifactId>
<version>f92124d750</version>
</dependency>
<dependency>
<groupId>co.bitpost</groupId>
<artifactId>interface-java</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
And add jitpack to your repositories:
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
Logging is commonly handled with logback. To configure it, change the file src/main/resources/logback.xml
. Below is an example:
<configuration debug="false">
<property name="ROOT_FOLDER" value="${user.dir}" />
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="org.bitcoinj" level="error">
<appender-ref ref="STDOUT" />
</logger>
<logger name="co.bitpost.BitpostRequest" additivity="false" level="debug">
<appender-ref ref="STDOUT" />
</logger>
<root level="debug">
<appender-ref ref="STDOUT" />
</root>
</configuration>
# Setting up your wallet
Let's start by creating a new private key and funding it.
import bit
key = bit.Key.from_bytes(b'REPLACE_WITH_YOUR_RANDOM_STRING')
print(key.segwit_address)
Tip: create a random key
You can print a cryptographically safe private key in python like this:
import os
import bit
random_bytes = os.urandom(16)
private_key_WIF = bit.Key.from_bytes(random_bytes).to_wif()
print(private_key_WIF)
You can then import the private key like this: key = bit.Key(private_key_WIF)
import bip32 = require("bip32");
import bip39 = require("bip39");
let mnemonicSeed: string = "your bip39 word list";
let seed = bip39.mnemonicToSeedSync(mnemonicSeed);
let master: bip32.BIP32Interface = bip32.fromSeed(
seed,
bitcoin.networks.bitcoin
);
let mainKey: bip32.BIP32Interface = master.derivePath(`m/84'/0'/0'/0/0`);
let changeKey: bip32.BIP32Interface = master.derivePath(`m/84'/0'/0'/1/0`);
let mainAddress = bitcoin.payments.p2wpkh({
pubkey: mainKey.publicKey,
network: bitcoin.networks.bitcoin,
});
let changeAddress = bitcoin.payments.p2wpkh({
pubkey: changeKey.publicKey,
network: bitcoin.networks.bitcoin,
});
console.log(mainAddress.address);
Tip: create a random key
let mnemonicSeed: string = bip39.generateMnemonic();
console.log(mnemonicSeed);
var mnemonicSeed = "your bip39 word list";
var seed = bip39.mnemonicToSeedSync(mnemonicSeed);
var master = bip32.fromSeed(seed, bitcoin.networks.bitcoin);
var mainKey = master.derivePath("m/84'/0'/0'/0/0");
var changeKey = master.derivePath("m/84'/0'/0'/1/0");
var mainAddress = bitcoin.payments.p2wpkh({
pubkey: mainKey.publicKey,
network: bitcoin.networks.bitcoin,
});
var changeAddress = bitcoin.payments.p2wpkh({
pubkey: changeKey.publicKey,
network: bitcoin.networks.bitcoin,
});
console.log(mainAddress.address);
Tip: create a random key
var mnemonicSeed = bip39.generateMnemonic();
console.log(mnemonicSeed);
import 'package:bitcoin_flutter/bitcoin_flutter.dart';
var randomWIF = 'YOUR_PRIAVTE_KEY_IN_WALLET_IMPORT_FORMAT_(WIF)';
var mainAddress = new P2WPKH(data: new PaymentData(pubkey: ECPair.fromWIF(randomWIF).publicKey)).data;
print(mainAddress);
Tip: create a random key
var randomWIF = ECPair.makeRandom().toWIF();
print(randomWIF);
import org.bitcoinj.script.Script;
import org.bitcoinj.params.MainNetParams;
import org.bitcoinj.wallet.DeterministicSeed;
import org.bitcoinj.wallet.Wallet;
String seedCode = "your bip39 word list";
long walletCreation = Instant.now().minusMillis(2*24*60*60*1000).getEpochSecond(); // used for bloom filtering
Path walletPath = Path.of(System.getProperty("user.dir"), "wallet.dat");
File walletFile = new File(walletPath.toString());
if(walletFile.exists()) {
Wallet wallet = Wallet.loadFromFile(walletFile);
} else {
DeterministicSeed seed = new DeterministicSeed(seedCode, null, "", walletCreation);
Wallet wallet = Wallet.fromSeed(MainNetParams.get(), seed, Script.ScriptType.P2WPKH);
}
Tip: create a random key
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Logger LOGGER = LoggerFactory.getLogger("Quick start mainnet");
Wallet wallet = Wallet.createDeterministic(MainNetParams.get(), Script.ScriptType.P2WPKH);
LOGGER.info(wallet.getKeyChainSeed().getMnemonicCode());
import bit
key = bit.PrivateKeyTestnet.from_bytes(b'REPLACE_WITH_YOUR_RANDOM_STRING')
print(key.segwit_address)
Tip: create a random key
You can print a cryptographically safe private key in python like this:
import os
import bit
random_bytes = os.urandom(16)
private_key_WIF = bit.PrivateKeyTestnet.from_bytes(random_bytes).to_wif()
print(private_key_WIF)
You can then import the private key like this: key = bit.PrivateKeyTestnet(private_key_WIF)
import bip32 = require("bip32");
import bip39 = require("bip39");
let mnemonicSeed: string = "your bip39 word list";
let seed = bip39.mnemonicToSeedSync(mnemonicSeed);
let master: bip32.BIP32Interface = bip32.fromSeed(
seed,
bitcoin.networks.testnet
);
let mainKey: bip32.BIP32Interface = master.derivePath(`m/84'/0'/0'/0/0`);
let changeKey: bip32.BIP32Interface = master.derivePath(`m/84'/0'/0'/1/0`);
var mainAddress = bitcoin.payments.p2wpkh({
pubkey: mainKey.publicKey,
network: bitcoin.networks.testnet,
});
var changeAddress = bitcoin.payments.p2wpkh({
pubkey: changeKey.publicKey,
network: bitcoin.networks.testnet,
});
console.log(mainAddress.address);
Tip: create a random key
let mnemonicSeed: string = bip39.generateMnemonic();
console.log(mnemonicSeed);
var mnemonicSeed = "your bip39 word list";
var seed = bip39.mnemonicToSeedSync(mnemonicSeed);
var master = bip32.fromSeed(seed, bitcoin.networks.testnet);
var mainKey = master.derivePath("m/84'/0'/0'/0/0");
var changeKey = master.derivePath("m/84'/0'/0'/1/0");
var mainAddress = bitcoin.payments.p2wpkh({
pubkey: mainKey.publicKey,
network: bitcoin.networks.testnet,
});
var changeAddress = bitcoin.payments.p2wpkh({
pubkey: changeKey.publicKey,
network: bitcoin.networks.testnet,
});
console.log(mainAddress.address);
Tip: create a random key
var mnemonicSeed = bip39.generateMnemonic();
console.log(mnemonicSeed);
import 'package:bitcoin_flutter/bitcoin_flutter.dart';
var randomWIF = 'YOUR_PRIAVTE_KEY_IN_WALLET_IMPORT_FORMAT_(WIF)';
var mainAddress = new P2WPKH(data: new PaymentData(pubkey: ECPair.fromWIF(randomWIF, network: testnet).publicKey)).data;
print(mainAddress);
Tip: create a random key
var randomWIF = ECPair.makeRandom(network: testnet).toWIF();
print(randomWIF);
import org.bitcoinj.script.Script;
import org.bitcoinj.params.MainNetParams;
import org.bitcoinj.wallet.DeterministicSeed;
import org.bitcoinj.wallet.Wallet;
String seedCode = "your bip39 word list";
long walletCreation = Instant.now().minusMillis(2*24*60*60*1000).getEpochSecond(); // used for bloom filtering
Path walletPath = Path.of(System.getProperty("user.dir"), "walletTestnet.dat");
File walletFile = new File(walletPath.toString());
if(walletFile.exists()) {
Wallet wallet = Wallet.loadFromFile(walletFile);
} else {
DeterministicSeed seed = new DeterministicSeed(seedCode, null, "", walletCreation);
Wallet wallet = Wallet.fromSeed(TestNet3Params.get(), seed, Script.ScriptType.P2WPKH);
}
Tip: create a random key
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Logger LOGGER = LoggerFactory.getLogger("Quick start testnet");
Wallet wallet = Wallet.createDeterministic(TestNet3Params.get(), Script.ScriptType.P2WPKH);
System.out.println(wallet.getKeyChainSeed().getMnemonicCode());
Send a small amount of bitcoin to it - 30,000 satoshis should be enough. On testnet, you can use this or that faucet. If can't get tBTC on neither faucet, asks us to send you. You don't need to wait for one confirmation to run the following code.
# Choose your payment
# Recipient, amount and maximum fee
Like any payment, you have to choose the recipient and the amount you want to pay. Then, instead of choosing the actual transaction fee, you simply have to choose a maximum you are willing to pay. This value represents the worst-case scenario and your transaction fee will likely be lower than that.
destination_address = '1BitcoinEaterAddressDontSendf59kuE'
sats_to_send = 566
max_dollar_fee = 3
const destinationAddress: string = "1BitcoinEaterAddressDontSendf59kuE";
const satsToSend: number = 566;
const maxDollarFee: number = 3;
var destinationAddress = "1BitcoinEaterAddressDontSendf59kuE";
var satsToSend = 566;
var maxDollarFee = 3;
var destination = '1BitcoinEaterAddressDontSendf59kuE';
var satsToSend = 566;
double maxDollarFee = 3;
import org.bitcoinj.core.*;
long satsToSend = 566;
Address destination = Address.fromString(MainNetParams.get(), "1BitcoinEaterAddressDontSendf59kuE");
int maxDollarFee = 3;
destination_address = 'mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt'
sats_to_send = 566
max_dollar_fee = 3
const destinationAddress: string = "mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt";
const satsToSend: number = 566;
const maxDollarFee: number = 3;
var destinationAddress = "mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt";
var satsToSend = 566;
var maxDollarFee = 3;
var destination = 'tb1qwp6gqlnxhtqum09z6z5t00ydzas8vcueuaps2a';
var satsToSend = 566;
double maxDollarFee = 3;
import org.bitcoinj.core.*;
long satsToSend = 566;
Address destination = Address.fromString(TestNet3Params.get(), "mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt");
int maxDollarFee = 3;
# Scheduling your transaction
The main parameter that Bitpost uses to control the transaction fee of your transaction is a parameter we call target, which indicates a deadline for the transaction to be included in a block (first confirmation). We also provide the optional parameter delay, which allows you to delay the first broadcast to a time of your choosing.
This way, you can schedule, for example, the payment of your rent between the 25th and the 30th of the next month like shown below.
import datetime as dt
target = int(dt.datetime(2020, 7, 30, 0, 0).timestamp())
delay = int(dt.datetime(2020, 7, 25, 0, 0).timestamp())
let target: number = Date.parse("2020-7-30 00:00");
let delay: number = Date.parse("2020-7-25 00:00");
var target = Date.parse("2020-7-30 00:00");
var delay = Date.parse("2020-7-25 00:00");
var target = (DateTime(2020, 7, 30, 0, 0).millisecondsSinceEpoch/1000).round();
var delay = (DateTime(2020, 7, 25, 0, 0).millisecondsSinceEpoch/1000).round();
import java.time.LocalDateTime;
import java.time.Month;
import java.time.ZoneOffset;
long target = LocalDateTime.of(2020, Month.JULY, 30, 00, 00).toEpochSecond(ZoneOffset.UTC);
long delay = LocalDateTime.of(2020, Month.JULY, 25, 00, 00).toEpochSecond(ZoneOffset.UTC);
# Preparing raw transactions
We will now sign several transactions that are identical except for the fee they pay. These transactions have to opt-in for replace-by-fee (RBF) for Bitpost to be able to adjust the fee.
# Choosing a set of feerates
We start by choosing the feerates of the transactions that will be relayed to Bitpost. You can opt for one of the two alternatives below after setting the user's maximum feerate. This step is discussed with more detail in this article.
Below is an approximate convertion between the user's choice for the maximum fee in fiat value and the maximum feerate. For a better approximation, use the results of your coin selection algorithm.
MAX_FEE_IN_SATS = bit.network.currency_to_satoshi(max_dollar_fee, 'usd')
HEURISTIC_TX_SIZE = 10 + 34*2 + 90 # 2 outputs and one P2SH-P2WKH input
USER_MAX_FEERATE = MAX_FEE_IN_SATS/HEURISTIC_TX_SIZE
const BTC_PRICE_USD: number = 10_000; //can be easily fetched through an API
const MAX_FEE_IN_SATS: number = (100_000_000 * maxDollarFee) / BTC_PRICE_USD;
const HEURISTIC_TX_SIZE = 10 + 34 * 2 + 68; // 2 outputs and one segwit input
const USER_MAX_FEERATE = MAX_FEE_IN_SATS / HEURISTIC_TX_SIZE;
var BTC_PRICE_USD = 10_000; //can be easily fetched through an API
var MAX_FEE_IN_SATS = (100_000_000 * maxDollarFee) / BTC_PRICE_USD;
var HEURISTIC_TX_SIZE = 10 + 34 * 2 + 68; // 2 outputs and one segwit input
var USER_MAX_FEERATE = MAX_FEE_IN_SATS / HEURISTIC_TX_SIZE;
var BTC*PRICE_USD = 10000; //can be easily fetched through an API
var MAX_FEE_IN_SATS = (100000000 * maxDollarFee) / BTC*PRICE_USD;
var HEURISTIC_TX_SIZE = 10 + 34 * 2 + 68; // 2 outputs and one segwit input
var USER_MAX_FEERATE = MAX_FEE_IN_SATS / HEURISTIC_TX_SIZE;
int BTC_PRICE_USD = 10_000; //can be easily fetched through an API
long MAX_FEE_IN_SATS = 100_000_000*maxDollarFee/BTC_PRICE_USD;
long HEURISTIC_TX_SIZE = 10 + 34*2 + 90; // 2 outputs and one P2SH-P2WKH input
double USER_MAX_FEERATE = 1.0*MAX_FEE_IN_SATS/HEURISTIC_TX_SIZE;
# With the /feerateset endpoint (recommended)
This endpoint provides a set of feerates that can be used for the transactions relayed to bitpost. These feerates consider the current fee environment and use that information to adjust the spacing between the feerates. They are, therefore, not linearly space like in the alternative below.
from bitpost.interface_for_bit import BitpostInterfaceForBit
bitpost_interface = BitpostInterfaceForBit()
feerates = BitpostInterfaceForBit.get_feerates(USER_MAX_FEERATE, size=50, target=target)
import { BitpostInterfaceForBitcoinJS, BitpostRequest } from "bitpost";
let bitpostInterface: BitpostInterfaceForBitcoinJS = new BitpostInterfaceForBitcoinJS(
{}
);
let feerates: Array<number> = bitpostInterface.getFeerates({
maxFeerate: USER_MAX_FEERATE,
size = 50,
target = target,
});
var bitpost = require("bitpost");
var bitpostInterface = new bitpost.BitpostInterfaceForBitcoinJS({});
var feerates = bitpostInterface.getFeerates({ maxFeerate: USER_MAX_FEERATE });
import 'package:bitpost/bitpost.dart';
var interface = BitpostInterface();
var feerates = await interface.getFeerates(USER_MAX_FEERATE);
import co.bitpost.BitpostInterface;
BitpostInterface bitpostInterface = new BitpostInterface();
List<Double> feerates = bitpostInterface.getFeerates(USER_MAX_FEERATE, 50, target, true);
from bitpost.interface_for_bit import BitpostInterfaceForBit
bitpost_interface = BitpostInterfaceForBit(testnet=True)
feerates = BitpostInterfaceForBit.get_feerates(USER_MAX_FEERATE, size=50, target=target)
import { BitpostInterfaceForBitcoinJS, BitpostRequest } from "bitpost";
let bitpostInterface: BitpostInterfaceForBitcoinJS = new BitpostInterfaceForBitcoinJS(
{ testnet: true }
);
let feerates: Array<number> = bitpostInterface.getFeerates({
maxFeerate: USER_MAX_FEERATE,
size = 50,
target = target,
});
var bitpost = require("bitpost");
var bitpostInterface = new bitpost.BitpostInterfaceForBitcoinJS({
testnet: true,
});
var feerates = bitpostInterface.getFeerates({ maxFeerate: USER_MAX_FEERATE });
import 'package:bitpost/bitpost.dart';
var interface = BitpostInterface(testnet: true);
var feerates = await interface.getFeerates(USER_MAX_FEERATE);
import co.bitpost.BitpostInterface;
BitpostInterface bitpostInterface = new BitpostInterface(true);
List<Double> feerates = bitpostInterface.getFeerates(USER_MAX_FEERATE, 50, target, true);
# Without the /feerateset endpoint
You can create a linearly spaced array of feerates with the code below. This approach works well in most cases but may lead to a poor choice of feerates that limit our ability to adjust the fee efficiently.
import numpy as np
# Adjust maximum feerate to avoid unnecessarily high feerates
MAX_FEERATE = int(min(USER_MAX_FEERATE, max(20, bit.network.get_fee(fast=True) * 3)))
DEFAULT_NUMBER_TXS = 50 # create 50 transactions with different fees
feerates = [int(feerate) for feerate in np.arange(1, MAX_FEERATE + 0.001, step=max(1, (MAX_FEERATE - 1) / (DEFAULT_NUMBER_TXS - 1)))]
# Signing raw transactions
For each feerate that we chose before, we sign one transaction that pays sats_to_send
to destination_address
. These transactions spend from the same inputs and therefore only one can be mined. It's not unsafe to send these all these transactions to a third party - the worst thing that can happen is for the transaction with USER_MAX_FEERATE
to be mined or no transaction at all. The last scenario can easily be avoided by the user, if he wants to (see next section).
# select inputs
unspents = key.get_unspents()
selected_unspents, _ = bit.transaction.select_coins(sats_to_send, MAX_FEERATE, [34], min_change=566, unspents=unspents)
raw_signed_txs = []
for feerate in feerates:
tx = key.create_transaction([(destination_address, sats_to_send, 'satoshi')], feerate, combine=True, replace_by_fee=True, unspents=selected_unspents)
raw_signed_txs.append(tx)
First, we need to fetch the wallet UTXOs for funding the transactions. In the code below we use Blockstream's API.
const request = require("sync-request");
interface TxInput {
txId: string;
vout: number;
value: number;
witnessUtxo: WitnessUtxo;
}
function getUtxosFromAddress(address: string): Array<TxInput> {
var resp = request(
"GET",
"https://blockstream.info/api/address/" + address + "/utxo"
);
let utxosRaw = JSON.parse(resp.getBody("utf-8"));
return utxosRaw.map((utxo) => {
utxos.push({
txId: utxo.txid,
vout: utxo.vout,
value: utxo.value,
witnessUtxo: {
script: bitcoin.address.toOutputScript(
address,
bitcoin.networks.bitcoin
),
value: utxo.value,
},
});
});
}
let utxos: Array<TxInput> = [];
utxos.concat(getUtxosFromAddress(mainAddress.address));
utxos.concat(getUtxosFromAddress(changeAddress.address));
We select which UTXOs we want to use. This coin selection algorithm underestimates the feerate of the transaction, so we multiply the feerate by 2/3
.
import coinSelect = require("coinselect");
let payees = [{ address: destinationAddress, value: satsToSend }];
let { inputs, outputs, _ } = coinSelect(
utxos,
payees,
Math.round((USER_MAX_FEERATE * 2) / 3)
);
We create first a preliminary transaction to know the size of our transactions.
interface TxOutput {
address: string;
value: number;
network: bitcoin.Network;
}
interface WitnessUtxo {
script: Buffer;
value: number;
}
function make_transaction(
inputs: Array<TxInput>,
outputs: Array<TxOutput>,
targetFeerate?: number,
txSize?: number
): bitcoin.Psbt {
if (Boolean(targetFeerate) !== Boolean(txSize))
throw "TargetFeerate and txSize arguments must be both provided or left in blank.";
let tx: bitcoin.Psbt = new bitcoin.Psbt({
network: bitcoin.networks.bitcoin,
});
if (targetFeerate) {
tx.setMaximumFeeRate(targetFeerate + 1);
}
let initialFee = 0;
inputs.forEach((input) => {
initialFee += input.value;
tx.addInput({
hash: input.txId,
index: input.vout,
sequence: 0xfffffffd,
witnessUtxo: {
script: input.witnessUtxo.script,
value: input.witnessUtxo.value,
},
});
});
outputs.forEach((o) => (initialFee -= o.value));
outputs.forEach((output) => {
if (!output.address && targetFeerate) {
const adjustedChange: number =
output.value + initialFee - txSize * targetFeerate;
tx.addOutput({ address: changeAddress.address, value: adjustedChange });
} else if (!output.address) {
tx.addOutput({ address: changeAddress.address, value: output.value });
} else {
tx.addOutput({ address: output.address, value: output.value });
}
});
for (let i = 0; i < inputs.length; i++) {
try {
tx.signInput(i, mainKey);
} catch {}
try {
tx.signInput(i, changeKey);
} catch {}
}
tx.validateSignaturesOfAllInputs();
tx.finalizeAllInputs();
return tx;
}
let preliminary_tx: bitcoin.Psbt = make_transaction(inputs, outputs);
let txSize: number = preliminary_tx.extractTransaction().virtualSize();
Now we create and sign a set of transactions which differ only in the fee(rate).
let rawTxs: Array<string> = [];
for (let feerate of feerates) {
let final_tx = make_transaction(inputs, outputs, feerate, txSize);
rawTxs.push(final_tx.extractTransaction().toHex());
}
First, we need to fetch the wallet UTXOs for funding the transactions. In the code below we use Blockstream's API.
var request = require("sync-request");
function getUtxosFromAddress(address) {
var resp = request(
"GET",
"https://blockstream.info/api/address/" + address + "/utxo"
);
var utxosRaw = JSON.parse(resp.getBody("utf-8"));
return utxosRaw.map(function(utxo) {
utxos.push({
txId: utxo.txid,
vout: utxo.vout,
value: utxo.value,
witnessUtxo: {
script: bitcoin.address.toOutputScript(
address,
bitcoin.networks.bitcoin
),
value: utxo.value,
},
});
});
}
var utxos = [];
utxos.concat(getUtxosFromAddress(mainAddress.address));
utxos.concat(getUtxosFromAddress(changeAddress.address));
We select which UTXOs we want to use. This coin selection algorithm underestimates the feerate of the transaction, so we multiply the feerate by 2/3
.
var coinSelect = require("coinselect");
var payees = [{ address: destinationAddress, value: satsToSend }];
var [inputs, outputs] = coinSelect(
utxos,
payees,
Math.round((USER_MAX_FEERATE * 2) / 3)
);
We create first a preliminary transaction to know the size of our transactions.
function make_transaction(inputs, outputs, targetFeerate, txSize) {
if (Boolean(targetFeerate) !== Boolean(txSize))
throw "TargetFeerate and txSize arguments must be both provided or left in blank.";
var tx = new bitcoin.Psbt({ network: bitcoin.networks.bitcoin });
if (targetFeerate) {
tx.setMaximumFeeRate(targetFeerate + 1);
}
var initialFee = 0;
inputs.forEach(function(input) {
initialFee += input.value;
tx.addInput({
hash: input.txId,
index: input.vout,
sequence: 0xfffffffd,
witnessUtxo: {
script: input.witnessUtxo.script,
value: input.witnessUtxo.value,
},
});
});
outputs.forEach(function(o) {
return (initialFee -= o.value);
});
outputs.forEach(function(output) {
if (!output.address && targetFeerate) {
var adjustedChange = output.value + initialFee - txSize * targetFeerate;
tx.addOutput({ address: changeAddress.address, value: adjustedChange });
} else if (!output.address) {
tx.addOutput({ address: changeAddress.address, value: output.value });
} else {
tx.addOutput({ address: output.address, value: output.value });
}
});
for (var i = 0; i < inputs.length; i++) {
try {
tx.signInput(i, mainKey);
} catch (_a) {}
try {
tx.signInput(i, changeKey);
} catch (_b) {}
}
tx.validateSignaturesOfAllInputs();
tx.finalizeAllInputs();
return tx;
}
var preliminary_tx = make_transaction(inputs, outputs);
var txSize = preliminary_tx.extractTransaction().virtualSize();
Now we create and sign a set of transactions which differ only in the fee(rate).
var rawTxs = [];
for (var i = 0; i < feerates.length; i++) {
var final_tx = make_transaction(inputs, outputs, feerates[i], txSize);
rawTxs.push(final_tx.extractTransaction().toHex());
}
We start by fetching the available UTXOs that can be spent by the wallet. For simplicity's sake, we will only consider one address and will spend all available UTXOs.
var response = await http.get('https://blockstream.info/api/address/' + mainAddress.address + '/utxo');
dynamic utxos = jsonDecode(response.body);
var totalInput = 0;
utxos.forEach((utxo) => totalInput += utxo['value']);
var size = 10 + 67*utxos.length + 34*2;
We then set create, sign and store one transaction for each feerate that we chose previously.
var rawTxs = List<String>();
for(double feerate in feerates) {
try {
var txBuilder = new TransactionBuilder(maximumFeeRate: (maximumFeerate*1.1).ceil());
txBuilder.setVersion(1);
utxos.forEach((utxo) =>
txBuilder.addInput(
utxo['txid'], utxo['vout'], 4294967293, mainAddress.output));
txBuilder.addOutput(destination, satsToSend);
var change = totalInput - feerate * size - satsToSend;
txBuilder.addOutput(mainAddress.address, change.ceil());
new List<int>.generate(txBuilder.inputs.length, (i) => i)
.forEach((index) =>
txBuilder.sign(vin: index,
keyPair: ECPair.fromWIF(randomWIF),
witnessValue: utxos[index]['value']));
rawTxs.add(txBuilder.build().toHex());
} catch (e){
// https://github.com/dart-bitcoin/bip32-dart/issues/5
print('Failed signing tx with feerate=${feerate}. This probably an issue of bip32-dart package: https://bit.ly/3qaxOM3');
}
}
First, we need to fetch the wallet UTXOs for funding the transactions. BitcoinJ is able to connect to other bitcoin nodes and get the UTXOs spendable by the wallet through Bloom filtering and simplified payment verification (SPV). For this, it needs to fetch all the block headers since the genesis block. Each header takes 80 bytes, making the whole block header chain ~50 MB on mainnet. This initial verification may take some time and therefore we save the final state in a blockHeaders.dat
file and save the wallet state to wallet.dat
.
Path blockStorePath = Path.of(System.getProperty("user.dir"), "blockHeaders.dat");
File blockFile = new File(blockStorePath.toString());
Wallet wallet = getWallet(walletFile);
BlockStore blockStore = new SPVBlockStore(MainNetParams.get(), blockFile, 20000, true);
BlockChain blockChain = new BlockChain(MainNetParams.get(), wallet, blockStore);
PeerGroup peerGroup = new PeerGroup(MainNetParams.get(), blockChain);
peerGroup.setBloomFilteringEnabled(true);
peerGroup.addWallet(wallet);
peerGroup.addPeerDiscovery(new DnsDiscovery(MainNetParams.get()));
peerGroup.start();
peerGroup.startBlockChainDownload(null);
peerGroup.connectToLocalHost();
peerGroup.setMaxConnections(3);
ListenableFuture<List<Peer>> peerFuture = peerGroup.waitForPeers(3);
LOGGER.debug("Saving block headers to file=" + blockFile.toString() + ", Peer group running=" + peerGroup.isRunning());
peerFuture.get();
while(blockChain.getBestChainHeight() < peerGroup.getMostCommonChainHeight()) {
Thread.sleep(10 * 1000);
LOGGER.debug("Block store height=" + blockChain.getBestChainHeight() + ", most common chain height="
+ peerGroup.getMostCommonChainHeight() + ", progress(%)=" + Math.round(1000.0*blockChain.getBestChainHeight()/peerGroup.getMostCommonChainHeight())/10.0 );
}
wallet.saveToFile(walletFile);
We first create a template for the payment we want to do. The wallet.completeTx()
selects the UTXOs to be used and we can use its output to know the size of our transaction.
import org.bitcoinj.wallet.SendRequest;
SendRequest srTemplate = SendRequest.to(destination, Coin.ofSat(satsToSend));
srTemplate.setFeePerVkb(Coin.ofSat(Math.round(USER_MAX_FEERATE*1000)));
srTemplate.shuffleOutputs = false; //so we know which one is the change, for the next step
wallet.completeTx(srTemplate);
We loop trough the feerates we want to use and set the change amount manually so that each transaction only differs in the fee it pays.
import org.apache.commons.codec.binary.Hex;
List<String> rawTxs = new ArrayList<>();
for(Double feerate : feerates) {
Transaction tx = new Transaction(MainNetParams.get());
srTemplate.tx.getInputs().forEach(tx::addInput);
tx.getInputs().forEach(i -> i.setSequenceNumber(4294967293L));
tx.addOutput(Coin.ofSat(satsToSend), destination);
long newFee = Math.round(feerate*srTemplate.tx.getVsize());
long feeDifference = srTemplate.tx.getFee().getValue() - newFee;
long changeAmount = srTemplate.tx.getOutput(1).getValue().getValue() + feeDifference;
tx.addOutput(Coin.ofSat(changeAmount), srTemplate.tx.getOutput(1).getScriptPubKey());
SendRequest newRequest = SendRequest.forTx(tx);
tx.getInput(0).clearScriptBytes();
wallet.signTransaction(newRequest);
rawTxs.add(Hex.encodeHexString(tx.bitcoinSerialize()));
}
# select inputs
unspents = key.get_unspents()
selected_unspents, _ = bit.transaction.select_coins(sats_to_send, MAX_FEERATE, [34], min_change=566, unspents=unspents)
raw_signed_txs = []
for feerate in feerates:
tx = key.create_transaction([(destination_address, sats_to_send, 'satoshi')], feerate, combine=True, replace_by_fee=True, unspents=selected_unspents)
raw_signed_txs.append(tx)
First, we need to fetch the wallet UTXOs for funding the transactions. In the code below we use Blockstream's API.
interface TxInput {
txId: string;
vout: number;
value: number;
witnessUtxo: WitnessUtxo;
}
function getUtxosFromAddress(address: string): Array<TxInput> {
var resp = request(
"GET",
"https://blockstream.info/testnet/api/address/" + address + "/utxo"
);
let utxosRaw = JSON.parse(resp.getBody("utf-8"));
return utxosRaw.map((utxo) => {
utxos.push({
txId: utxo.txid,
vout: utxo.vout,
value: utxo.value,
witnessUtxo: {
script: bitcoin.address.toOutputScript(
address,
bitcoin.networks.testnet
),
value: utxo.value,
},
});
});
}
let utxos: Array<TxInput> = [];
utxos.concat(getUtxosFromAddress(mainAddress.address));
utxos.concat(getUtxosFromAddress(changeAddress.address));
We select which UTXOs we want to use. This coin selection algorithm underestimates the feerate of the transaction, so we multiply the feerate by 2/3
.
let payees = [{ address: destinationAddress, value: satsToSend }];
let { inputs, outputs, _ } = coinSelect(
utxos,
payees,
Math.round((USER_MAX_FEERATE * 2) / 3)
);
We create first a preliminary transaction to know the size of our transactions.
interface TxOutput {
address: string;
value: number;
network: bitcoin.Network;
}
interface WitnessUtxo {
script: Buffer;
value: number;
}
function make_transaction(
inputs: Array<TxInput>,
outputs: Array<TxOutput>,
targetFeerate?: number,
txSize?: number
): bitcoin.Psbt {
if (Boolean(targetFeerate) !== Boolean(txSize))
throw "TargetFeerate and txSize arguments must be both provided or left in blank.";
let tx: bitcoin.Psbt = new bitcoin.Psbt({
network: bitcoin.networks.testnet,
});
if (targetFeerate) {
tx.setMaximumFeeRate(targetFeerate + 1);
}
let initialFee = 0;
inputs.forEach((input) => {
initialFee += input.value;
tx.addInput({
hash: input.txId,
index: input.vout,
sequence: 0xfffffffd,
witnessUtxo: {
script: input.witnessUtxo.script,
value: input.witnessUtxo.value,
},
});
});
outputs.forEach((o) => (initialFee -= o.value));
outputs.forEach((output) => {
if (!output.address && targetFeerate) {
const adjustedChange: number =
output.value + initialFee - txSize * targetFeerate;
tx.addOutput({ address: changeAddress.address, value: adjustedChange });
} else if (!output.address) {
tx.addOutput({ address: changeAddress.address, value: output.value });
} else {
tx.addOutput({ address: output.address, value: output.value });
}
});
for (let i = 0; i < inputs.length; i++) {
try {
tx.signInput(i, mainKey);
} catch {}
try {
tx.signInput(i, changeKey);
} catch {}
}
tx.validateSignaturesOfAllInputs();
tx.finalizeAllInputs();
return tx;
}
let preliminary_tx: bitcoin.Psbt = make_transaction(inputs, outputs);
let txSize: number = preliminary_tx.extractTransaction().virtualSize();
Now we create and sign a set of transactions which differ only in the fee(rate).
let rawTxs: Array<string> = [];
for (let feerate of feerates) {
let final_tx = make_transaction(inputs, outputs, feerate, txSize);
rawTxs.push(final_tx.extractTransaction().toHex());
}
First, we need to fetch the wallet UTXOs for funding the transactions. In the code below we use Blockstream's API.
var request = require("sync-request");
function getUtxosFromAddress(address) {
var resp = request(
"GET",
"https://blockstream.info/testnet/api/address/" + address + "/utxo"
);
var utxosRaw = JSON.parse(resp.getBody("utf-8"));
return utxosRaw.map(function(utxo) {
utxos.push({
txId: utxo.txid,
vout: utxo.vout,
value: utxo.value,
witnessUtxo: {
script: bitcoin.address.toOutputScript(
address,
bitcoin.networks.testnet
),
value: utxo.value,
},
});
});
}
var utxos = [];
utxos.concat(getUtxosFromAddress(mainAddress.address));
utxos.concat(getUtxosFromAddress(changeAddress.address));
We select which UTXOs we want to use. This coin selection algorithm underestimates the feerate of the transaction, so we multiply the feerate by 2/3
.
var coinSelect = require("coinselect");
var payees = [{ address: destinationAddress, value: satsToSend }];
var { inputs, outputs, _ } = coinSelect(
utxos,
payees,
Math.round((USER_MAX_FEERATE * 2) / 3)
);
We create first a preliminary transaction to know the size of our transactions.
function make_transaction(inputs, outputs, targetFeerate, txSize) {
if (Boolean(targetFeerate) !== Boolean(txSize))
throw "TargetFeerate and txSize arguments must be both provided or left in blank.";
var tx = new bitcoin.Psbt({ network: bitcoin.networks.testnet });
if (targetFeerate) {
tx.setMaximumFeeRate(targetFeerate + 1);
}
var initialFee = 0;
inputs.forEach(function(input) {
initialFee += input.value;
tx.addInput({
hash: input.txId,
index: input.vout,
sequence: 0xfffffffd,
witnessUtxo: {
script: input.witnessUtxo.script,
value: input.witnessUtxo.value,
},
});
});
outputs.forEach(function(o) {
return (initialFee -= o.value);
});
outputs.forEach(function(output) {
if (!output.address && targetFeerate) {
var adjustedChange = output.value + initialFee - txSize * targetFeerate;
tx.addOutput({ address: changeAddress.address, value: adjustedChange });
} else if (!output.address) {
tx.addOutput({ address: changeAddress.address, value: output.value });
} else {
tx.addOutput({ address: output.address, value: output.value });
}
});
for (var i = 0; i < inputs.length; i++) {
try {
tx.signInput(i, mainKey);
} catch (_a) {}
try {
tx.signInput(i, changeKey);
} catch (_b) {}
}
tx.validateSignaturesOfAllInputs();
tx.finalizeAllInputs();
return tx;
}
var preliminary_tx = make_transaction(inputs, outputs);
var txSize = preliminary_tx.extractTransaction().virtualSize();
Now we create and sign a set of transactions which differ only in the fee(rate).
var rawTxs = [];
for (var i = 0; i < feerates.length; i++) {
var final_tx = make_transaction(inputs, outputs, feerates[i], txSize);
rawTxs.push(final_tx.extractTransaction().toHex());
}
We start by fetching the available UTXOs that can be spent by the wallet. For simplicity's sake, we will only consider one address and will spend all available UTXOs.
var response = await http.get('https://blockstream.info/testnet/api/address/' + mainAddress.address + '/utxo');
dynamic utxos = jsonDecode(response.body);
var totalInput = 0;
utxos.forEach((utxo) => totalInput += utxo['value']);
var size = 10 + 67*utxos.length + 34*2;
We then set create, sign and store one transaction for each feerate that we chose previously.
var rawTxs = List<String>();
for(double feerate in feerates) {
try {
var txBuilder = new TransactionBuilder(
network: testnet, maximumFeeRate: (maximumFeerate*1.1).ceil());
txBuilder.setVersion(1);
utxos.forEach((utxo) =>
txBuilder.addInput(
utxo['txid'], utxo['vout'], 4294967293, mainAddress.output));
txBuilder.addOutput(destination, satsToSend);
var change = totalInput - feerate * size - satsToSend;
txBuilder.addOutput(mainAddress.address, change.ceil());
new List<int>.generate(txBuilder.inputs.length, (i) => i)
.forEach((index) =>
txBuilder.sign(vin: index,
keyPair: ECPair.fromWIF(randomWIF, network: testnet),
witnessValue: utxos[index]['value']));
rawTxs.add(txBuilder.build().toHex());
} catch (e){
// https://github.com/dart-bitcoin/bip32-dart/issues/5
print('Failed signing tx with feerate=${feerate}. This is an issue of bip32-dart package.');
}
}
First, we need to fetch the wallet UTXOs for funding the transactions. BitcoinJ is able to connect to other bitcoin nodes and get the UTXOs spendable by the wallet through Bloom filtering and simplified payment verification (SPV). For this, it needs to fetch all the block headers since the genesis block. Each header takes 80 bytes, making the whole block header chain ~150 MB on testnet. This initial verification may take some time and therefore we save the final state in a blockHeadersTestnet.dat
file and save the wallet state to walletTestnet.dat
.
Path blockStorePath = Path.of(System.getProperty("user.dir"), "blockHeadersTestnet.dat");
File blockFile = new File(blockStorePath.toString());
Wallet wallet = getWallet(walletFile);
BlockStore blockStore = new SPVBlockStore(TestNet3Params.get(), blockFile, 20000, true);
BlockChain blockChain = new BlockChain(TestNet3Params.get(), wallet, blockStore);
PeerGroup peerGroup = new PeerGroup(TestNet3Params.get(), blockChain);
peerGroup.setBloomFilteringEnabled(true);
peerGroup.addWallet(wallet);
peerGroup.addPeerDiscovery(new DnsDiscovery(TestNet3Params.get()));
peerGroup.start();
peerGroup.startBlockChainDownload(null);
peerGroup.connectToLocalHost();
peerGroup.setMaxConnections(3);
ListenableFuture<List<Peer>> peerFuture = peerGroup.waitForPeers(3);
LOGGER.debug("Saving block headers to file=" + blockFile.toString() + ", Peer group running=" + peerGroup.isRunning());
peerFuture.get();
while(blockChain.getBestChainHeight() < peerGroup.getMostCommonChainHeight()) {
Thread.sleep(10 * 1000);
LOGGER.debug("Block store height=" + blockChain.getBestChainHeight() + ", most common chain height="
+ peerGroup.getMostCommonChainHeight() + ", progress(%)=" + Math.round(1000.0*blockChain.getBestChainHeight()/peerGroup.getMostCommonChainHeight())/10.0 );
}
wallet.saveToFile(walletFile);
We first create a template for the payment we want to do. The wallet.completeTx()
selects the UTXOs to be used and we can use its output to know the size of our transaction.
import org.bitcoinj.wallet.SendRequest;
SendRequest srTemplate = SendRequest.to(destination, Coin.ofSat(satsToSend));
srTemplate.setFeePerVkb(Coin.ofSat(Math.round(USER_MAX_FEERATE*1000)));
srTemplate.shuffleOutputs = false; //so we know which one is the change, for the next step
wallet.completeTx(srTemplate);
We loop trough the feerates we want to use and set the change amount manually so that each transaction only differs in the fee it pays.
import org.apache.commons.codec.binary.Hex;
List<String> rawTxs = new ArrayList<>();
for(Double feerate : feerates) {
Transaction tx = new Transaction(Testnet3Params.get());
srTemplate.tx.getInputs().forEach(tx::addInput);
tx.getInputs().forEach(i -> i.setSequenceNumber(4294967293L));
tx.addOutput(Coin.ofSat(satsToSend), destination);
long newFee = Math.round(feerate*srTemplate.tx.getVsize());
long feeDifference = srTemplate.tx.getFee().getValue() - newFee;
long changeAmount = srTemplate.tx.getOutput(1).getValue().getValue() + feeDifference;
tx.addOutput(Coin.ofSat(changeAmount), srTemplate.tx.getOutput(1).getScriptPubKey());
SendRequest newRequest = SendRequest.forTx(tx);
tx.getInput(0).clearScriptBytes();
wallet.signTransaction(newRequest);
rawTxs.add(Hex.encodeHexString(tx.bitcoinSerialize()));
}
# Controling the initial broadcast
By default, Bitpost will choose when and which transaction to broadcast first. If it's a low priority transaction and fees are high, it might take a long time for Bitpost to make the first broadcast. If you want to prevent this and force Bitpost to broadcast one transaction as soon as it receives the request, set delay=0
(the default being delay=1
). Alternatively, if you want Bitpost to (re)broadcast a specific transaction (which you might have broadcasted yourself), you can pass the txid of the broadcasted transaction to the parameter broadcast.
# Sending your request to Bitpost
You can send the request for the automatic fee adjustment of your transaction by calling our API directly or using an interface class if available for the language you are using.
Below we use the python interface:
request = bitpost_interface.create_bitpost_request(raw_signed_txs, target, delay=0)
request.send_request()
const delay = 0;
let bitpostRequest: BitpostRequest = bitpostInterface.createBitpostRequest(
rawTxs,
target,
delay
);
let response = bitpostRequest.sendRequest();
var delay = 0;
var bitpostRequest = bitpostInterface.createBitpostRequest(
rawTxs,
target,
delay
);
var response = bitpostRequest.sendRequest();
var delay = 0;
var bitpostRequest = interface.createBitpostRequest(rawTxs, target, delay: delay);
bitpostRequest.sendRequest();
import kong.unirest.json.JSONObject;
long delay = 0;
BitpostRequest bitpostRequest = bitpostInterface.createBitpostRequest(rawTxs, target, delay, false);
JSONObject response = bitpostRequest.sendRequest();
Our answer is structured in JSend.
{
"data": {
"id": "R9RJJBLz8Kd",
"url": "https://bitpost.co/explorer?query=R9RJJBLz8Kd",
"devurl": "https://api.bitpost.co/request?id=R9RJJBLz8Kd"
},
"status": "success"
}
# Tracking your request
If you open the URL contained in the answer, you can follow all the broadcasts and other events related to your fee adjustment request in our explorer. You can bump the fee yourself - although you don't need to - and it will show (with up to 5 min delay).
If you forget the URL of the request, you can query instead by a txid
that belongs to the request. It doesn't necessarily need to be a broadcasted transaction or the one currently present in the mempool.
# Further reading
- Check some common errors that might occur when you send your request to Bitpost.
- Check how to create child requests with Bitpost.