/* * Copyright 2013 Google Inc. * Copyright 2014 Andreas Schildbach * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.bitcoinj.examples; import joptsimple.OptionParser; import joptsimple.OptionSet; import joptsimple.OptionSpec; import org.bitcoinj.core.*; import org.bitcoinj.kits.WalletAppKit; import org.bitcoinj.params.RegTestParams; import org.bitcoinj.protocols.channels.*; import org.bitcoinj.utils.BriefLogFormatter; import org.bitcoinj.utils.Threading; import org.bitcoinj.wallet.Wallet; import org.bitcoinj.wallet.WalletExtension; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.Uninterruptibles; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; import java.net.InetSocketAddress; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import static org.bitcoinj.core.Coin.CENT; /** * Simple client that connects to the given host, opens a channel, and pays one cent. */ public class ExamplePaymentChannelClient { private static final org.slf4j.Logger log = LoggerFactory.getLogger(ExamplePaymentChannelClient.class); private WalletAppKit appKit; private final Coin channelSize; private final ECKey myKey; private final NetworkParameters params; public static void main(String[] args) throws Exception { BriefLogFormatter.init(); OptionParser parser = new OptionParser(); OptionSpec<NetworkEnum> net = parser.accepts("net", "The network to run the examples on").withRequiredArg().ofType(NetworkEnum.class).defaultsTo(NetworkEnum.TEST); OptionSpec<Integer> version = parser.accepts("version", "The payment channel protocol to use").withRequiredArg().ofType(Integer.class); parser.accepts("help", "Displays program options"); OptionSet opts = parser.parse(args); if (opts.has("help") || !opts.has(net) || opts.nonOptionArguments().size() != 1) { System.err.println("usage: ExamplePaymentChannelClient --net=MAIN/TEST/REGTEST --version=1/2 host"); parser.printHelpOn(System.err); return; } IPaymentChannelClient.ClientChannelProperties clientChannelProperties = new PaymentChannelClient.DefaultClientChannelProperties(){ @Override public PaymentChannelClient.VersionSelector versionSelector() { return PaymentChannelClient.VersionSelector.VERSION_1; } }; if (opts.has("version")) { switch (version.value(opts)) { case 1: // Keep the default break; case 2: clientChannelProperties = new PaymentChannelClient.DefaultClientChannelProperties(){ @Override public PaymentChannelClient.VersionSelector versionSelector() { return PaymentChannelClient.VersionSelector.VERSION_2; } }; break; default: System.err.println("Invalid version - valid versions are 1, 2"); return; } } NetworkParameters params = net.value(opts).get(); new ExamplePaymentChannelClient().run(opts.nonOptionArguments().get(0), clientChannelProperties, params); } public ExamplePaymentChannelClient() { channelSize = CENT; myKey = new ECKey(); params = RegTestParams.get(); } public void run(final String host, IPaymentChannelClient.ClientChannelProperties clientChannelProperties, final NetworkParameters params) throws Exception { // Bring up all the objects we need, create/load a wallet, sync the chain, etc. We override WalletAppKit so we // can customize it by adding the extension objects - we have to do this before the wallet file is loaded so // the plugin that knows how to parse all the additional data is present during the load. appKit = new WalletAppKit(params, new File("."), "payment_channel_example_client") { @Override protected List<WalletExtension> provideWalletExtensions() { // The StoredPaymentChannelClientStates object is responsible for, amongst other things, broadcasting // the refund transaction if its lock time has expired. It also persists channels so we can resume them // after a restart. // We should not send a PeerGroup in the StoredPaymentChannelClientStates constructor // since WalletAppKit will find it for us. return ImmutableList.<WalletExtension>of(new StoredPaymentChannelClientStates(null)); } }; // Broadcasting can take a bit of time so we up the timeout for "real" networks final int timeoutSeconds = params.getId().equals(NetworkParameters.ID_REGTEST) ? 15 : 150; if (params == RegTestParams.get()) { appKit.connectToLocalHost(); } appKit.startAsync(); appKit.awaitRunning(); // We now have active network connections and a fully synced wallet. // Add a new key which will be used for the multisig contract. appKit.wallet().importKey(myKey); appKit.wallet().allowSpendingUnconfirmedTransactions(); System.out.println(appKit.wallet()); // Create the object which manages the payment channels protocol, client side. Tell it where the server to // connect to is, along with some reasonable network timeouts, the wallet and our temporary key. We also have // to pick an amount of value to lock up for the duration of the channel. // // Note that this may or may not actually construct a new channel. If an existing unclosed channel is found in // the wallet, then it'll re-use that one instead. final InetSocketAddress server = new InetSocketAddress(host, 4242); waitForSufficientBalance(channelSize); final String channelID = host; // Do this twice as each one sends 1/10th of a bitcent 5 times, so to send a bitcent, we do it twice. This // demonstrates resuming a channel that wasn't closed yet. It should close automatically once we run out // of money on the channel. log.info("Round one ..."); openAndSend(timeoutSeconds, server, channelID, 5, clientChannelProperties); log.info("Round two ..."); log.info(appKit.wallet().toString()); openAndSend(timeoutSeconds, server, channelID, 4, clientChannelProperties); // 4 times because the opening of the channel made a payment. log.info("Stopping ..."); appKit.stopAsync(); appKit.awaitTerminated(); } private void openAndSend(int timeoutSecs, InetSocketAddress server, String channelID, final int times, IPaymentChannelClient.ClientChannelProperties clientChannelProperties) throws IOException, ValueOutOfRangeException, InterruptedException { // Use protocol version 1 for simplicity PaymentChannelClientConnection client = new PaymentChannelClientConnection( server, timeoutSecs, appKit.wallet(), myKey, channelSize, channelID, null, clientChannelProperties); // Opening the channel requires talking to the server, so it's asynchronous. final CountDownLatch latch = new CountDownLatch(1); Futures.addCallback(client.getChannelOpenFuture(), new FutureCallback<PaymentChannelClientConnection>() { @Override public void onSuccess(PaymentChannelClientConnection client) { // By the time we get here, if the channel is new then we already made a micropayment! The reason is, // we are not allowed to have payment channels that pay nothing at all. log.info("Success! Trying to make {} micropayments. Already paid {} satoshis on this channel", times, client.state().getValueSpent()); final Coin MICROPAYMENT_SIZE = CENT.divide(10); for (int i = 0; i < times; i++) { try { // Wait because the act of making a micropayment is async, and we're not allowed to overlap. // This callback is running on the user thread (see the last lines in openAndSend) so it's safe // for us to block here: if we didn't select the right thread, we'd end up blocking the payment // channels thread and would deadlock. Uninterruptibles.getUninterruptibly(client.incrementPayment(MICROPAYMENT_SIZE)); } catch (ValueOutOfRangeException e) { log.error("Failed to increment payment by a CENT, remaining value is {}", client.state().getValueRefunded()); throw new RuntimeException(e); } catch (ExecutionException e) { log.error("Failed to increment payment", e); throw new RuntimeException(e); } log.info("Successfully sent payment of one CENT, total remaining on channel is now {}", client.state().getValueRefunded()); } if (client.state().getValueRefunded().compareTo(MICROPAYMENT_SIZE) < 0) { // Now tell the server we're done so they should broadcast the final transaction and refund us what's // left. If we never do this then eventually the server will time out and do it anyway and if the // server goes away for longer, then eventually WE will time out and the refund tx will get broadcast // by ourselves. log.info("Settling channel for good"); client.settle(); } else { // Just unplug from the server but leave the channel open so it can resume later. client.disconnectWithoutSettlement(); } latch.countDown(); } @Override public void onFailure(Throwable throwable) { log.error("Failed to open connection", throwable); latch.countDown(); } }, Threading.USER_THREAD); latch.await(); } private void waitForSufficientBalance(Coin amount) { // Not enough money in the wallet. Coin amountPlusFee = amount.add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE); // ESTIMATED because we don't really need to wait for confirmation. ListenableFuture<Coin> balanceFuture = appKit.wallet().getBalanceFuture(amountPlusFee, Wallet.BalanceType.ESTIMATED); if (!balanceFuture.isDone()) { System.out.println("Please send " + amountPlusFee.toFriendlyString() + " to " + myKey.toAddress(params)); Futures.getUnchecked(balanceFuture); } } }