-
Notifications
You must be signed in to change notification settings - Fork 101
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: restore payment channel examples
- Loading branch information
1 parent
b555ede
commit 294f28e
Showing
3 changed files
with
354 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
217 changes: 217 additions & 0 deletions
217
examples/src/main/java/org/bitcoinj/examples/ExamplePaymentChannelClient.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,217 @@ | ||
|
||
/* | ||
* 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((String) 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 " + Address.fromKey(params, myKey)); | ||
Futures.getUnchecked(balanceFuture); | ||
} | ||
} | ||
} |
135 changes: 135 additions & 0 deletions
135
examples/src/main/java/org/bitcoinj/examples/ExamplePaymentChannelServer.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
/* | ||
* 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.Coin; | ||
import org.bitcoinj.core.NetworkParameters; | ||
import org.bitcoinj.core.Sha256Hash; | ||
import org.bitcoinj.core.VerificationException; | ||
import org.bitcoinj.kits.WalletAppKit; | ||
import org.bitcoinj.params.RegTestParams; | ||
import org.bitcoinj.protocols.channels.*; | ||
import org.bitcoinj.utils.BriefLogFormatter; | ||
import org.bitcoinj.wallet.WalletExtension; | ||
|
||
import com.google.common.collect.ImmutableList; | ||
|
||
import com.google.common.util.concurrent.ListenableFuture; | ||
import com.google.protobuf.ByteString; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import java.io.File; | ||
import java.net.SocketAddress; | ||
import java.util.List; | ||
|
||
/** | ||
* Simple server that listens on port 4242 for incoming payment channels. | ||
*/ | ||
public class ExamplePaymentChannelServer implements PaymentChannelServerListener.HandlerFactory { | ||
private static final org.slf4j.Logger log = LoggerFactory.getLogger(ExamplePaymentChannelServer.class); | ||
|
||
private WalletAppKit appKit; | ||
|
||
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); | ||
parser.accepts("help", "Displays program options"); | ||
OptionSet opts = parser.parse(args); | ||
if (opts.has("help") || !opts.has(net)) { | ||
System.err.println("usage: ExamplePaymentChannelServer --net=MAIN/TEST/REGTEST"); | ||
parser.printHelpOn(System.err); | ||
return; | ||
} | ||
NetworkParameters params = net.value(opts).get(); | ||
new ExamplePaymentChannelServer().run(params); | ||
} | ||
|
||
public void run(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_server") { | ||
@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. | ||
return ImmutableList.<WalletExtension>of(new StoredPaymentChannelServerStates(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(); | ||
|
||
System.out.println(appKit.wallet()); | ||
|
||
// We provide a peer group, a wallet, a timeout in seconds, the amount we require to start a channel and | ||
// an implementation of HandlerFactory, which we just implement ourselves. | ||
new PaymentChannelServerListener(appKit.peerGroup(), appKit.wallet(), timeoutSeconds, Coin.valueOf(100000), this).bindAndStart(4242); | ||
} | ||
|
||
@Override | ||
public ServerConnectionEventHandler onNewConnection(final SocketAddress clientAddress) { | ||
// Each connection needs a handler which is informed when that payment channel gets adjusted. Here we just log | ||
// things. In a real app this object would be connected to some business logic. | ||
return new ServerConnectionEventHandler() { | ||
@Override | ||
public void channelOpen(Sha256Hash channelId) { | ||
log.info("Channel open for {}: {}.", clientAddress, channelId); | ||
|
||
// Try to get the state object from the stored state set in our wallet | ||
PaymentChannelServerState state = null; | ||
try { | ||
StoredPaymentChannelServerStates storedStates = (StoredPaymentChannelServerStates) | ||
appKit.wallet().getExtensions().get(StoredPaymentChannelServerStates.class.getName()); | ||
state = storedStates.getChannel(channelId).getOrCreateState(appKit.wallet(), appKit.peerGroup()); | ||
} catch (VerificationException e) { | ||
// This indicates corrupted data, and since the channel was just opened, cannot happen | ||
throw new RuntimeException(e); | ||
} | ||
log.info(" with a maximum value of {}, expiring at UNIX timestamp {}.", | ||
// The channel's maximum value is the value of the multisig contract which locks in some | ||
// amount of money to the channel | ||
state.getContract().getOutput(0).getValue(), | ||
// The channel expires at some offset from when the client's refund transaction becomes | ||
// spendable. | ||
state.getExpiryTime() + StoredPaymentChannelServerStates.CHANNEL_EXPIRE_OFFSET); | ||
} | ||
|
||
@Override | ||
public ListenableFuture<ByteString> paymentIncrease(Coin by, Coin to, ByteString info) { | ||
log.info("Client {} paid increased payment by {} for a total of " + to.toString(), clientAddress, by); | ||
return null; | ||
} | ||
|
||
@Override | ||
public void channelClosed(PaymentChannelCloseException.CloseReason reason) { | ||
log.info("Client {} closed channel for reason {}", clientAddress, reason); | ||
} | ||
}; | ||
} | ||
} |