BIP47 Support Ready for Popular Open-Source Java Libraries

Wallet developers working with Java will now have a much easier time implementing BIP47 Reusable Payment Codes into their projects.

The engineering team at Stash has recently submitted pull requests to include Java implementations of BIP47 in two popular open-source wallet libraries: BitcoinJ (for BTC) & BitcoinJ-Cash (for BCH).

Special thanks to Carlos Lopez for his hard work on this!

We believe that the paycode protocol offers significant usability and privacy improvements for all cryptocurrency users and that by supporting the broad implementation of this open standard, we can further promote mainstream adoption.

BIP47 Overview

BIP47 allows users to generate a single, static paycode ID that can be openly published for receiving confidential cryptocurrency payments on multiple blockchains. It eliminates privacy-leaking address reuse, while still allowing recipients to identify the origin of incoming payments without the cumbersome exchange of unique deposit addresses with each payment.

Paycodes are essentially BIP32 and BIP44 compatible addresses with some additional version and feature bytes. The following is Stash's Donation Paycode as an alphanumeric string and scannable QR code.


PM8TJXzwp4KJSCfj4688VztGiwEjAAu6shpyaET9wSBs7HdW7GPrMsxfeKRfWcvnAFf4SdVztB1YFUMmBWLZ5TZZFAE9Sui8pSZ5mL7w5aAnpUMP2vEv

official-stash-paycode-qr-wide

To make a payment, a sender's wallet generates a shared secret behind the scenes using elliptic-curve Diffie-Hellman on keys derived from the sender's private key and the recipient's paycode. Each shared secret helps form a deterministic sequence of addresses both parties can observe, but only the recipient can spend from.

alice-pays-bob-3f

For the recipient to identify an incoming payment and who it came from, a one-time notification on the blockchain is all that's required. This allows the recipient to rederive all payment channel funds from the wallet's primary BIP39 recovery seed.

Version 1

The sender initiates the payment channel by spending the minimum allowed amount (546 satoshis) in an OP_RETURN notification transaction to the recipient. -- currently implemented

Version 2

The change script of the notification transaction is modified so that the amount transfered is destined for the payer. In this way, the output to the notification address of the recipient is completely eliminated.

Version 3

The reusability of payment addresses is generalized to be used in Bitcoin and in other blockchains too.


Using BIP47AppKit

BIP47AppKit is the Java class that allows wallets to send and receive BIP47 payments. It takes network and coin name parameters to support various coins, such as BTC, tBTC, BCH or tBCH. It also takes a folder as a parameter to save the wallet, blockstore and bip47 metadata files.

Creating a New BIP47 Wallet


// javac Create.java -cp *.jar; java -cp bitcoinj-bundle.jar:. Create
import org.bitcoinj.core.*;
import org.bitcoinj.params.TestNet3Params;
import java.io.File;
import org.bitcoinj.kits.BIP47AppKit;
import org.bitcoinj.wallet.bip47.listeners.BlockchainDownloadProgressTracker;
import java.util.*;
 
class Create {
 public static void main(String as[]) throws Exception {
  String coin = "tBTC";
  NetworkParameters params = TestNet3Params.get();
  File dir = new File("My BIP47 Wallets");

  System.out.println("Creating new " + coin + " wallet ...");
  Context.propagate(new Context(params));
  BIP47AppKit bip47App = new BIP47AppKit(coin, params, dir, null);
  System.out.println("done. Payment code: " + bip47App.getPaymentCode());
  
  // the download will not start unless there is an event listener
  class Tracker extends BlockchainDownloadProgressTracker{
   public Tracker(String coin){
    super(coin);
   }
   @Override
   public int getProgress() {
    return 42;
   }
  }
  System.out.println("Adding progress tracker ...");
  bip47App.setBlockchainDownloadProgressTracker(new Tracker(coin));

  System.out.println("Starting " + coin + " blockchain download ...");
  bip47App.startBlockchainDownload();

  while (true) {
   int lastHeight = bip47App.getvWallet().getLastBlockSeenHeight();
   List peers = bip47App.getConnectedPeers();
   System.out.print("Last height: "+lastHeight+" Balance: "+bip47App.getvWallet().getBalance()+" Peers: "+peers.size()+"\r");
  }
 }
}

// groovy -cp bitcoincashj-core-0.14.7-bip47.3-bundled.jar create.groovy
import org.bitcoinj.core.*
import org.bitcoinj.params.*;
import org.bitcoinj.kits.*
import org.bitcoinj.wallet.bip47.listeners.BlockchainDownloadProgressTracker

coin = "tBTC";
params = TestNet3Params.get();
dir = new File("My BIP47 Wallets");

Context.propagate(new Context(params));
println("Creating new " + coin + " wallet ...");
bip47App = new BIP47AppKit(coin, params, dir, null);
println("done. Payment code: " + bip47App.getPaymentCode());

// the download will not start unless there is an event listener
class Tracker extends BlockchainDownloadProgressTracker{
 private double percentage;
 public Tracker(String coin){
  super(coin);
 }
 int getProgress() {
  return 42;
 }
}

println("Adding progress tracker ...")
bip47App.setBlockchainDownloadProgressTracker(new Tracker())

println("Starting " + coin + " blockchain download ...")
bip47App.startBlockchainDownload()

while (true) {
 lastHeight = bip47App.vWallet.getLastBlockSeenHeight();
 List peers = bip47App.getConnectedPeers();
 print("Last height: ${lastHeight} Balance: ${bip47App.vWallet.getBalance()} Peers: ${peers.size}\r")
}
// jjs create.js -cp bitcoinj-bundle.jar --print-no-newline
bcj = org.bitcoinj;
core = bcj.core;

var coin = "tBTC"
var params = bcj.params.TestNet3Params.get();
var dir = new java.io.File("My BIP47 Wallets");

core.Context.propagate(new core.Context(params));
print("Creating new " + coin + " wallet ...");
var bip47App = new bcj.kits.BIP47AppKit(coin, params, dir, null)
print("done. Payment code: " + bip47App.getPaymentCode());

var ProgressTracker = bcj.wallet.bip47.listeners.BlockchainDownloadProgressTracker

// the download will not start unless there is an event listener
var Tracker = Java.extend(bcj.wallet.bip47.listeners.BlockchainDownloadProgressTracker, {
    "<init>": function(coin) {
    }
});

print("Adding progress tracker ...\n")
bip47App.setBlockchainDownloadProgressTracker(new Tracker(coin))


print("Starting blockchain download ...\n")
bip47App.startBlockchainDownload()

while (true) {
    var lastHeight = bip47App.vWallet.getLastBlockSeenHeight();
    var peers = bip47App.getConnectedPeers();
    print("Last height: "+lastHeight+" Balance: "+bip47App.vWallet.getBalance()+" Peers: "+peers.length+"\r")
}

Restoring a BIP47 Wallet from Seed

import org.bitcoinj.wallet.DeterministicSeed;
import org.bitcoinj.crypto.*;

String mnemonic = "response seminar brave tip suit recall often sound stick owner lottery motion";
long bip39bornDate = MnemonicCode.BIP39_STANDARDISATION_TIME_SECS;
DeterministicSeed seed = new DeterministicSeed(mnemonic, null, "", bip39bornDate);

Context.propagate(new Context(params));
BIP47AppKit bip47App = new BIP47AppKit(coin, params, dir, seed);
System.out.println("Payment code restored: " + bip47App.getPaymentCode());
import org.bitcoinj.wallet.DeterministicSeed;
import org.bitcoinj.crypto.*;

mnemonic = "response seminar brave tip suit recall often sound stick owner lottery motion";
bip39bornDate = MnemonicCode.BIP39_STANDARDISATION_TIME_SECS;
seed = new DeterministicSeed(mnemonic, null, "", bip39bornDate);

Context.propagate(new Context(params));
bip47App = new BIP47AppKit(coin, params, dir, seed);
println("Payment code restored: " + bip47App.getPaymentCode());
wallet = org.bitcoinj.wallet;
crypto = org.bitcoinj.crypto;

bip39bornDate = crypto.MnemonicCode.BIP39_STANDARDISATION_TIME_SECS;
mnemonic = "response seminar brave tip suit recall often sound stick owner lottery motion";
seed = new seed.DeterministicSeed(mnemonic, null, "", bip39bornDate);
core.Context.propagate(new core.Context(params));
bip47App = new bcj.kits.BIP47AppKit(coin, params, dir, seed);
print("Payment code restored: " + bip47App.getPaymentCode());

Sending a Transaction to a Paycode

When the wallet creates or receives a BIP47 payment for the first time, a .bip47 file is created that contains the serialized payment channels. Every payment channel contains information such as whether the wallet has sent a notification transaction back, a list of deposit addresses seen, receiving payments, and addresses used for sending payments.

String payee = "PM8TJS2JxQ5ztXUpBBRnpTbcUXbUHy2T1abfrb3KkAAtMEGNbey4oumH7Hc578WgQJhPjBxteQ5GHHToTYHE3A1w6p7tU6KSoFmWBVbFGjKPisZDbP97"
Bip47AppKit payer = new BIP47AppKit(coin, params, dir, seed)
SendRequest notification = payer.makeNotificationTransaction(payee)
Runnable callback = new Runnable() {
 @Override
  public void run() {
   System.out.println("Tx sent.");
  }
}
Executor executor = Executors.newSingleThreadExecutor();
System.out.println("Broadcasting notification tx to: " + payee);
payer.broadcastTransaction(notification.tx).addListener(callback, executor);
payer.putPaymenCodeStatusSent(payee, notification.tx);
BIP47Channel paymentChannel = payer.getBip47MetaForPaymentCode(payee);
Address depositAddress;
if (paymentChannel != null && paymentChannel.isNotificationTransactionSent()) {
   depositAddress = payer.getCurrentOutgoingAddress(paymentChannel);
}
Coin amount = Coin.valueOf(1000);
SendRequest payment = wallet.createSend(depositAddress, amount);
System.out.println("Sending payment of 1000 satoshis to: " + payee);
payer.broadcastTransaction(payment.tx).addListener(callback, executor);
payee = "PM8TJS2JxQ5ztXUpBBRnpTbcUXbUHy2T1abfrb3KkAAtMEGNbey4oumH7Hc578WgQJhPjBxteQ5GHHToTYHE3A1w6p7tU6KSoFmWBVbFGjKPisZDbP97"
payer = new BIP47AppKit(coin, params, dir, seed)
notification = payer.makeNotificationTransaction(payee)
callback = new Runnable() {
 @Override
  public void run() {
   System.out.println("Tx sent.");
  }
}
executor = Executors.newSingleThreadExecutor();
println("Broadcasting notification tx to: " + payee);
payer.broadcastTransaction(notification.tx).addListener(callback, executor);
payer.putPaymenCodeStatusSent(payee, notification.tx);
paymentChannel = payer.getBip47MetaForPaymentCode(payee);
def depositAddress;
if (paymentChannel != null && paymentChannel.isNotificationTransactionSent()) {
 depositAddress = payer.getCurrentOutgoingAddress(paymentChannel);
}
amount = Coin.valueOf(1000);
payment = wallet.createSend(depositAddress, amount);
println("Sending payment of 1000 satoshis to: " + payee);
payer.broadcastTransaction(payment.tx).addListener(callback, executor);
payee = "PM8TJS2JxQ5ztXUpBBRnpTbcUXbUHy2T1abfrb3KkAAtMEGNbey4oumH7Hc578WgQJhPjBxteQ5GHHToTYHE3A1w6p7tU6KSoFmWBVbFGjKPisZDbP97"
payer = new BIP47AppKit(coin, params, dir, seed)
notification = payer.makeNotificationTransaction(payee)
var Runnable = Java.type("");
callback = Java.extend(java.lang.Runnable, {
 "run": function() { 
  println("Tx sent.");
 }
});
executor = Executors.newSingleThreadExecutor();
print("Broadcasting notification tx to: " + payee);
payer.broadcastTransaction(notification.tx).addListener(callback, executor);
payer.putPaymenCodeStatusSent(payee, notification.tx);
paymentChannel = payer.getBip47MetaForPaymentCode(payee);
var depositAddress;
if (paymentChannel != null && paymentChannel.isNotificationTransactionSent()){
  depositAddress = payer.getCurrentOutgoingAddress(paymentChannel);
}
amount = Coin.valueOf(1000);
payment = wallet.createSend(depositAddress, amount);
print("Sending payment of 1000 satoshis to: " + payee);
payer.broadcastTransaction(payment.tx).addListener(callback, executor); 

Listening for BIP47 Transactions


import org.bitcoinj.wallet.bip47.listeners.TransactionEventListener;

TransactionEventListener bip47Listener = new TransactionEventListener() {
 public void onTransactionReceived(BIP47AppKit app, Transaction tx) {
  String paymentCode;
  if (app.isNotificationTransaction(tx)) {
    paymentCode = app.getPaymentCodeInNotificationTransaction(tx);
    System.out.println("Received notification tx from payment code: "+ paymentCode);
  } else if (app.isToBIP47Address(tx)) {
    String depositAddress = app.getAddressOfReceived(tx).toString();
    paymentCode = wallet.getPaymentCodeForAddress(depositAddress);
    System.out.println("Received payment from payment code: " + paymentCode);
  }
 }
}

bip47App.addOnReceiveTransactionListener(bip47Listener);
import org.bitcoinj.wallet.bip47.listeners.TransactionEventListener

bip47Listener = new TransactionEventListener() {
 @Override
 public void onTransactionReceived(BIP47AppKit app, Transaction tx) {
  def paymentCode;
  if (app.isNotificationTransaction(tx)) {
   paymentCode = app.getPaymentCodeInNotificationTransaction(tx);
   println("Received notification from payment code: "+ paymentCode);
  } else if (app.isToBIP47Address(tx)) {
   String depositAddress = app.getAddressOfReceived(tx).toString();
   paymentCode = app.getPaymentCodeForAddress(depositAddress);
   println("Received payment from payment code: " + paymentCode)
  }
 }
}

bip47App.addOnReceiveTransactionListener(bip47Listener);
wallet = org.bitcoinj.wallet;

bip47Listener = Java.extend(wallet.bip47.listeners.TransactionEventListener,  {
 "onTransactionReceived": function(app, tx) { 
  var paymentCode;
  if (app.isNotificationTransaction(tx)) {
   paymentCode = app.getPaymentCodeInNotificationTransaction(tx);
   print("Received notification from payment code: "+ paymentCode);
  } else if (app.isToBIP47Address(tx)) {
   depositAddress = app.getAddressOfReceived(tx).toString();
   paymentCode = app.getPaymentCodeForAddress(depositAddress);
   print("Received payment from payment code: " + paymentCode)
  }
 }
}

bip47App.addOnReceiveTransactionListener(bip47Listener);

Summary of Benefits

  • One static reusable address supporting multiple cryptocurrencies.
  • Wallet balances are never exposed to 3rd party blockchain observers.
  • Payments remain confidential between the sender and recipient.
  • Inbound payments contain all info needed for easy refunds.
  • All payment channels can be restored from a single seed.
  • No reliance on other out-of-band third party services.

Join our Slack Channel

If you're a wallet developer and would like to participate in an industry working group for implementing BIP47, please send us a request via email or Twitter for an invitation link!


Resources

  1. BitcoinJ Pull Request
  2. BitcoinJ-Cash Pull Request
  3. BIP47 Specification
  4. Version 3 Notifications
  5. Original Announcement on Reddit
  6. Blog Post by Billion Wallet
  7. Stash Wallet for Android
  8. Stash Homepage
Show Comments

Get the latest posts delivered right to your inbox.