/*
* Copyright 2013 Google Inc.
*
* 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 com.google.devcoin.examples;
import com.google.devcoin.core.*;
import com.google.devcoin.kits.WalletAppKit;
import com.google.devcoin.params.TestNet3Params;
import com.google.devcoin.protocols.channels.PaymentChannelClientConnection;
import com.google.devcoin.protocols.channels.StoredPaymentChannelClientStates;
import com.google.devcoin.protocols.channels.ValueOutOfRangeException;
import com.google.devcoin.utils.BriefLogFormatter;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.math.BigInteger;
import java.net.InetSocketAddress;
import java.util.concurrent.CountDownLatch;
import static com.google.devcoin.core.Utils.CENT;
import static java.math.BigInteger.TEN;
import static java.math.BigInteger.ZERO;
/**
* 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 BigInteger channelSize;
private final ECKey myKey;
private final NetworkParameters params;
public static void main(String[] args) throws Exception {
BriefLogFormatter.init();
System.out.println("USAGE: host");
new ExamplePaymentChannelClient().run(args[0]);
}
public ExamplePaymentChannelClient() {
channelSize = CENT;
myKey = new ECKey();
params = TestNet3Params.get();
}
public void run(final String host) 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 void addWalletExtensions() {
// 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.
wallet().addExtension(new StoredPaymentChannelClientStates(wallet(), peerGroup()));
}
};
appKit.startAndWait();
// 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().addKey(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 int timeoutSecs = 15;
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(timeoutSecs, server, channelID);
log.info("Round two ...");
log.info(appKit.wallet().toString());
openAndSend(timeoutSecs, server, channelID);
log.info("Waiting ...");
Thread.sleep(60 * 60 * 1000); // 1 hour.
log.info("Stopping ...");
appKit.stopAndWait();
}
private void openAndSend(int timeoutSecs, InetSocketAddress server, String channelID) throws IOException, ValueOutOfRangeException, InterruptedException {
PaymentChannelClientConnection client = new PaymentChannelClientConnection(
server, timeoutSecs, appKit.wallet(), myKey, channelSize, channelID);
// 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) {
// Success! We should be able to try making micropayments now. Try doing it 5 times.
for (int i = 0; i < 5; i++) {
try {
client.incrementPayment(CENT.divide(TEN));
} catch (ValueOutOfRangeException e) {
log.error("Failed to increment payment by a CENT, remaining value is {}", client.state().getValueRefunded());
System.exit(-3);
}
log.info("Successfully sent payment of one CENT, total remaining on channel is now {}", client.state().getValueRefunded());
}
if (client.state().getValueRefunded().equals(ZERO)) {
// 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("Closing channel for good");
client.close();
} else {
// Just unplug from the server but leave the channel open so it can resume later.
client.disconnectWithoutChannelClose();
}
latch.countDown();
}
@Override
public void onFailure(Throwable throwable) {
log.error("Failed to open connection", throwable);
latch.countDown();
}
});
latch.await();
}
private void waitForSufficientBalance(BigInteger amount) {
// Not enough money in the wallet.
BigInteger amountPlusFee = amount.add(Wallet.SendRequest.DEFAULT_FEE_PER_KB);
// ESTIMATED because we don't really need to wait for confirmation.
ListenableFuture<BigInteger> balanceFuture = appKit.wallet().getBalanceFuture(amountPlusFee, Wallet.BalanceType.ESTIMATED);
if (!balanceFuture.isDone()) {
System.out.println("Please send " + Utils.bitcoinValueToFriendlyString(amountPlusFee) +
" BTC to " + myKey.toAddress(params));
Futures.getUnchecked(balanceFuture);
}
}
}