The bitcoinj library is easy to use for Bitcoin wallet and transaction functions for both native Java and Android applications. Although there are certain features missing, it seems mature enough to be included in a Bitcoin walled app or service.

Sometimes the source code leaves a bit to be desired in structure and readability: anonymous inner classes and other deep nesting blocks sometimes makes it difficult to follow; inheritance is often used where composition would have been be better; the Collections classes could have been used over arrays in many places. All of this might come back to haunt the developers later, for now they seem to be plowing on.

At least the basics are straight forward. The following code will read a test walled from disk, or create a new one if it does not already exist. The TestNet3 block chain and network is used. Since the bitcoinj library relies heavily on the Google Guava (com.google.common) classes, there are frequent artifacts of the threading and callback handling showing up. In this example, we want the code to block and wait, therefore the extra await-functions are required.

  public void testSync() {
    params = TestNet3Params.get();
    kit = new WalletAppKit(params, new File("/dev/shm"), "test");

    kit.startAsync();
    kit.awaitRunning();

    kit.stopAsync();
    kit.awaitTerminated();
  }

The nice thing about the test block chain is that it is a real public live block chain, with miners and a block chain explorer, but with no value in the coins. In fact, you can get free coins to test with from faucet.xeno-genesis.com or tpfaucet.appspot.com. (The latter has been timing out over the last days).

To get some free test coins, run the following code, wait for the prompt which shows the next receiving address, and head over to faucet.xeno-genesis.com to ask them to send some money there. It should show up as received within a few seconds. Your wallet now contains some coins.

  public void testReceive() {
    startSync();

    wallet.addCoinsReceivedEventListener(this::coinsReceived);

    Address toAdr = wallet.currentReceiveKey().toAddress(params);
    System.out.println("Waiting to receive coins on: " + toAdr);

    while (!receivedCoins) {
      sleep(100);
    }
  }

  private void coinsReceived(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) {
    Coin value = tx.getValueSentToMe(wallet);
    System.out.println("Received tx for " + value.toFriendlyString() + ": " + tx);
    receivedCoins = true;
  }

Since the test network is a real network with real miners, it’s good etiquette to return your test coins to the pool for others to use once you’re done with them. The following code takes care of that, returning them to the TP Faucet default return address “n2eMqTT929pb1RDNuqEnxdaLau1rxy3efi”. You can return all your coins, or just a fraction if you want to experiment more. This will also wait a few seconds for the callback confirmation.

  public void testSend() throws InsufficientMoneyException {
    startSync();

    // Adjust how many coins to send. E.g. the minimum; or everything.
    Coin sendValue = Transaction.REFERENCE_DEFAULT_MIN_TX_FEE;
    // Coin sendValue = wallet.getBalance().minus(Transaction.DEFAULT_TX_FEE);
    
    Address sendToAdr = Address.fromBase58(params, TPFAUCET_RETURN_ADR);
    SendRequest request = SendRequest.to(sendToAdr, sendValue);

    SendResult result = wallet.sendCoins(request);

    result.broadcastComplete.addListener(() -> {
      println("Coins were sent. Transaction hash: " + result.tx.getHashAsString());
      sentCoins = true;
    }, MoreExecutors.sameThreadExecutor());

    while (!sentCoins) {
      sleep(100);
    }
  }

Finally, it’s worth noting that bitcoinj is a “live” library, in development and with the latest update available through Gradle. To make this work, there’s a few settings and dependencies to take care of. The logging framework used by bitcoinj is SL4J, and an actual implementation library (e.g. “sl4j-simple”) is also need. It can be downloaded, or included as a Gradle build dependency as seen below.

Then, your source code directory structure might not match the default Gradle “main”, “test” structure. My current structure keeps all source code under the directory “src”, so I have specified that.

Gradle integrates OK with Eclipse, but be careful with the “refresh” option. It tends to insist on changing the classpath setting of the project, so the packages disappear. It’s a good idea to keep the .classpath setting file under version control.

apply plugin: 'java'

sourceSets {
  main {
    java {
      srcDirs = ['src']
    }
  }
}

repositories {
  jcenter()
}

dependencies {
  compile 'org.slf4j:slf4j-api:1.7.21'
  compile 'org.slf4j:slf4j-simple:1.7.21'
  compile 'org.bitcoinj:bitcoinj-core:0.14.3'

  testCompile 'junit:junit:4.12'
}

The following listing shows all the tests. It demonstrates similar functionality as seen in the ForwardingService class in the main bitcoinj getting started guide. Hopefully, the code is a bit easier to read and run this way.

BitcoinjApiTest.java
GitHub Raw
/* Copyright rememberjava.com. Licensed under GPL 3. See http://rememberjava.com/license */
package com.rememberjava.bitcoin;

import java.io.File;

import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.InsufficientMoneyException;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.kits.WalletAppKit;
import org.bitcoinj.params.TestNet3Params;
import org.bitcoinj.wallet.SendRequest;
import org.bitcoinj.wallet.Wallet;
import org.bitcoinj.wallet.Wallet.SendResult;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.impl.StaticLoggerBinder;

import com.google.common.util.concurrent.MoreExecutors;

/**
 * Multiple large file and network I/O tests for the Testnet3 block chain.
 * Comment in and out @Test annotations as need.
 * 
 * For more info:<br>
 * https://en.bitcoin.it/wiki/Testnet<br>
 * 
 * For test coins: <br>
 * http://faucet.xeno-genesis.com <br>
 * http://tpfaucet.appspot.com <br>
 * 
 * For test block explorer: <br>
 * https://testnet.blockexplorer.com
 *
 */
public class BitcoinjApiTest {

  /**
   * http://tpfaucet.appspot.com
   */
  private static final String TPFAUCET_RETURN_ADR = "n2eMqTT929pb1RDNuqEnxdaLau1rxy3efi";

  private static final File WALLET_DIR = new File("/tmp");

  private static final String WALLET_PREFIX = "rjtest";

  private NetworkParameters params;

  private WalletAppKit kit;

  private Wallet wallet;

  private volatile boolean receivedCoins;

  private volatile boolean sentCoins;

  /**
   * For all tests, connect to the TestNet3 network.
   */
  @Before
  public void setup() {
    params = TestNet3Params.get();
    kit = new WalletAppKit(params, WALLET_DIR, WALLET_PREFIX);
  }

  /**
   * Connect, sync and wait till done.
   */
  private void startSync() {
    kit.startAsync();
    kit.awaitRunning();
    sleep(1000);

    wallet = kit.wallet();
  }

  /**
   * Disconnect and wait till done.
   */
  @After
  public void teardown() {
    kit.stopAsync();
    kit.awaitTerminated();
  }

  /**
   * Even though it is possible to run without the logger implementation classes
   * in place, it is very confusing and misleading.
   * 
   * The bitcoinj logs output connection and transaction information. Without
   * them, it's difficult to know what is happening, and the application might
   * seem "stuck".
   */
  @Test
  public void checkLogger() {
    StaticLoggerBinder.getSingleton();
  }

  /**
   * Connects to the TestNet3 network and disconnects. If there is no walled
   * stored at the specified location, a new one is created and relevant blocks
   * are downloaded. This can take 1 to 2 minutes. If information is stale, it
   * can also take a minute to update.
   * 
   * The walled used in this test is separate from the other tests.
   */
  @Test
  public void testSync() {
    params = TestNet3Params.get();
    kit = new WalletAppKit(params, new File("/dev/shm"), "test");

    kit.startAsync();
    kit.awaitRunning();

    kit.stopAsync();
    kit.awaitTerminated();
  }

  /**
   * Connects and prints address information about the test wallet (from the
   * TestNet3 network).
   */
  @Test
  public void printAddresses() {
    startSync();

    System.out.println("Receive Addresses:");
    wallet.getIssuedReceiveAddresses().stream().forEach(this::println);

    System.out.println("Watched Addresses:");
    wallet.getWatchedAddresses().stream().forEach(this::println);

    System.out.println("Current change address:");
    System.out.println(wallet.currentChangeAddress());
  }

  /**
   * Connects and prints wallet information (from the TestNet3 network).
   */
  @Test
  public void printWalletInfo() {
    startSync();

    Coin balance = wallet.getBalance();
    println("Balance satoshis: " + balance.toString());
    println("Balance friendly: " + balance.toFriendlyString());
    println("Version: " + wallet.getVersion());
  }

  /**
   * Waits for coins to be received on the prompted address. This test blocks
   * until the coinsReceived callback is called.
   * 
   * (This test is commented out to avoid blocking the other tests).
   */
  // @Test
  public void testReceive() {
    startSync();

    wallet.addCoinsReceivedEventListener(this::coinsReceived);

    Address toAdr = wallet.currentReceiveKey().toAddress(params);
    System.out.println("Waiting to receive coins on: " + toAdr);

    while (!receivedCoins) {
      sleep(100);
    }
  }

  /**
   * Callback called when the coins have been received.
   */
  private void coinsReceived(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) {
    Coin value = tx.getValueSentToMe(wallet);
    System.out.println("Received tx for " + value.toFriendlyString() + ": " + tx);
    receivedCoins = true;
  }

  /**
   * Sends test coins back to the TPFAUCET test network address. This test
   * blocks until the broadcastComplete callback is called.
   * 
   * (This test is commented out to avoid blocking the other test and
   * inadvertently sending away our money.
   */
  // @Test
  public void testSend() throws InsufficientMoneyException {
    startSync();

    // Adjust how many coins to send. E.g. the minimum; or everything.
    Coin sendValue = Transaction.REFERENCE_DEFAULT_MIN_TX_FEE;
    // Coin sendValue = wallet.getBalance().minus(Transaction.DEFAULT_TX_FEE);
    
    Address sendToAdr = Address.fromBase58(params, TPFAUCET_RETURN_ADR);
    SendRequest request = SendRequest.to(sendToAdr, sendValue);

    SendResult result = wallet.sendCoins(request);

    result.broadcastComplete.addListener(() -> {
      println("Coins were sent. Transaction hash: " + result.tx.getHashAsString());
      sentCoins = true;
    }, MoreExecutors.sameThreadExecutor());

    while (!sentCoins) {
      sleep(100);
    }
  }

  private void sleep(long millis) {
    try {
      Thread.sleep(millis);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }

  private void println(Object str) {
    System.out.println(str);
  }
}