mirror of
https://github.com/Qortal/altcoinj.git
synced 2025-02-07 14:54:15 +00:00
Adding support for processing PaymentRequests.
This commit is contained in:
parent
4ca476ff35
commit
3966875e8e
@ -57,6 +57,11 @@ public abstract class NetworkParameters implements Serializable {
|
||||
/** Unit test network. */
|
||||
public static final String ID_UNITTESTNET = "com.google.bitcoin.unittest";
|
||||
|
||||
/** The string used by the payment protocol to represent the main net. */
|
||||
public static final String PAYMENT_PROTOCOL_ID_MAINNET = "main";
|
||||
/** The string used by the payment protocol to represent the test net. */
|
||||
public static final String PAYMENT_PROTOCOL_ID_TESTNET = "test";
|
||||
|
||||
// TODO: Seed nodes should be here as well.
|
||||
|
||||
protected Block genesisBlock;
|
||||
@ -173,6 +178,8 @@ public abstract class NetworkParameters implements Serializable {
|
||||
return id;
|
||||
}
|
||||
|
||||
public abstract String getPaymentProtocolId();
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (!(other instanceof NetworkParameters)) return false;
|
||||
@ -199,6 +206,18 @@ public abstract class NetworkParameters implements Serializable {
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the network parameters for the given string paymentProtocolID or NULL if not recognized. */
|
||||
@Nullable
|
||||
public static NetworkParameters fromPmtProtocolID(String pmtProtocolId) {
|
||||
if (pmtProtocolId.equals(PAYMENT_PROTOCOL_ID_MAINNET)) {
|
||||
return MainNetParams.get();
|
||||
} else if (pmtProtocolId.equals(PAYMENT_PROTOCOL_ID_TESTNET)) {
|
||||
return TestNet3Params.get();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public int getSpendableCoinbaseDepth() {
|
||||
return spendableCoinbaseDepth;
|
||||
}
|
||||
|
@ -72,4 +72,8 @@ public class MainNetParams extends NetworkParameters {
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public String getPaymentProtocolId() {
|
||||
return PAYMENT_PROTOCOL_ID_MAINNET;
|
||||
}
|
||||
}
|
||||
|
@ -64,4 +64,8 @@ public class RegTestParams extends TestNet2Params {
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public String getPaymentProtocolId() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -55,4 +55,8 @@ public class TestNet2Params extends NetworkParameters {
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public String getPaymentProtocolId() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -62,4 +62,8 @@ public class TestNet3Params extends NetworkParameters {
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public String getPaymentProtocolId() {
|
||||
return PAYMENT_PROTOCOL_ID_TESTNET;
|
||||
}
|
||||
}
|
||||
|
@ -53,4 +53,8 @@ public class UnitTestParams extends NetworkParameters {
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public String getPaymentProtocolId() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,93 @@
|
||||
/**
|
||||
* 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.bitcoin.protocols.payments;
|
||||
|
||||
public class PaymentRequestException extends Exception {
|
||||
public PaymentRequestException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
public PaymentRequestException(Exception e) {
|
||||
super(e);
|
||||
}
|
||||
|
||||
public static class Expired extends PaymentRequestException {
|
||||
public Expired(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
public static class InvalidPaymentRequestURL extends PaymentRequestException {
|
||||
public InvalidPaymentRequestURL(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
public InvalidPaymentRequestURL(Exception e) {
|
||||
super(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class InvalidPaymentURL extends PaymentRequestException {
|
||||
public InvalidPaymentURL(Exception e) {
|
||||
super(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class InvalidOutputs extends PaymentRequestException {
|
||||
public InvalidOutputs(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
public static class InvalidVersion extends PaymentRequestException {
|
||||
public InvalidVersion(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
public static class InvalidNetwork extends PaymentRequestException {
|
||||
public InvalidNetwork(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
public static class InvalidPkiType extends PaymentRequestException {
|
||||
public InvalidPkiType(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
public static class InvalidPkiData extends PaymentRequestException {
|
||||
public InvalidPkiData(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
public InvalidPkiData(Exception e) {
|
||||
super(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class PkiVerificationException extends PaymentRequestException {
|
||||
public PkiVerificationException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
public PkiVerificationException(Exception e) {
|
||||
super(e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,597 @@
|
||||
/**
|
||||
* 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.bitcoin.protocols.payments;
|
||||
|
||||
import com.google.bitcoin.core.*;
|
||||
import com.google.bitcoin.params.MainNetParams;
|
||||
import com.google.bitcoin.params.TestNet3Params;
|
||||
import com.google.bitcoin.script.ScriptBuilder;
|
||||
import com.google.bitcoin.uri.BitcoinURI;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.ListeningExecutorService;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import com.google.protobuf.ByteString;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
import org.bitcoin.protocols.payments.Protos;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.spongycastle.asn1.ASN1String;
|
||||
import org.spongycastle.asn1.x500.AttributeTypeAndValue;
|
||||
import org.spongycastle.asn1.x500.RDN;
|
||||
import org.spongycastle.asn1.x500.style.RFC4519Style;
|
||||
import org.spongycastle.asn1.x500.X500Name;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.security.auth.x500.X500Principal;
|
||||
import java.io.*;
|
||||
import java.math.BigInteger;
|
||||
import java.net.*;
|
||||
import java.security.*;
|
||||
import java.security.cert.*;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/**
|
||||
* <p>Provides a standard implementation of the Payment Protocol (BIP 0070)</p>
|
||||
*
|
||||
* <p>A PaymentSession can be initialized from one of the following:</p>
|
||||
*
|
||||
* <ul>
|
||||
* <li>A {@link BitcoinURI} object that conforms to BIP 0072</li>
|
||||
* <li>A url where the {@link Protos.PaymentRequest} can be fetched</li>
|
||||
* <li>Directly with a {@link Protos.PaymentRequest} object</li>
|
||||
* </ul>
|
||||
*
|
||||
* If initialized with a BitcoinURI or a url, a network request is made for the payment request object and a
|
||||
* ListenableFuture is returned that will be notified with the PaymentSession object after it is downloaded.
|
||||
*
|
||||
* Once the PaymentSession is initialized, typically a wallet application will prompt the user to confirm that the
|
||||
* amount and recipient are correct, perform any additional steps, and then construct a list of transactions to pass to
|
||||
* the sendPayment method.
|
||||
*
|
||||
* Call sendPayment with a list of transactions that will be broadcast. A {@link Protos.Payment} message will be sent to
|
||||
* the merchant if a payment url is provided in the PaymentRequest.
|
||||
* NOTE: sendPayment does NOT broadcast the transactions to the bitcoin network.
|
||||
*
|
||||
* sendPayment returns a ListenableFuture that will be notified when a {@link Protos.PaymentACK} is received from the
|
||||
* merchant. Typically a wallet will show the message to the user as a confirmation message that the payment is now
|
||||
* "processing" or that an error occurred.
|
||||
*
|
||||
* @author Kevin Greene
|
||||
* @see <a href="https://github.com/bitcoin/bips/blob/master/bip-0070.mediawiki">BIP 0070</a>
|
||||
*/
|
||||
public class PaymentSession {
|
||||
private static final Logger log = LoggerFactory.getLogger(PaymentSession.class);
|
||||
private ListeningExecutorService executor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
|
||||
private NetworkParameters params;
|
||||
private String trustStorePath;
|
||||
private Protos.PaymentRequest paymentRequest;
|
||||
private Protos.PaymentDetails paymentDetails;
|
||||
private BigInteger totalValue = BigInteger.ZERO;
|
||||
|
||||
/**
|
||||
* Stores the calculated PKI verification data, or null if none is available.
|
||||
* Only valid after the session is created with verifyPki set to true, or verifyPki() is manually called.
|
||||
*/
|
||||
public PkiVerificationData pkiVerificationData;
|
||||
|
||||
/**
|
||||
* Returns a future that will be notified with a PaymentSession object after it is fetched using the provided uri.
|
||||
* uri is a BIP-72-style BitcoinURI object that specifies where the {@link Protos.PaymentRequest} object may
|
||||
* be fetched in the r= parameter.
|
||||
* If the payment request object specifies a PKI method, then the system trust store will
|
||||
* be used to verify the signature provided by the payment request. An exception is thrown by the future if the
|
||||
* signature cannot be verified.
|
||||
*/
|
||||
public static ListenableFuture<PaymentSession> createFromBitcoinUri(final BitcoinURI uri) throws PaymentRequestException {
|
||||
return createFromBitcoinUri(uri, true, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a future that will be notified with a PaymentSession object after it is fetched using the provided uri.
|
||||
* uri is a BIP-72-style BitcoinURI object that specifies where the {@link Protos.PaymentRequest} object may
|
||||
* be fetched in the r= parameter.
|
||||
* If verifyPki is specified and the payment request object specifies a PKI method, then the system trust store will
|
||||
* be used to verify the signature provided by the payment request. An exception is thrown by the future if the
|
||||
* signature cannot be verified.
|
||||
*/
|
||||
public static ListenableFuture<PaymentSession> createFromBitcoinUri(final BitcoinURI uri, final boolean verifyPki)
|
||||
throws PaymentRequestException {
|
||||
return createFromBitcoinUri(uri, verifyPki, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a future that will be notified with a PaymentSession object after it is fetched using the provided uri.
|
||||
* uri is a BIP-72-style BitcoinURI object that specifies where the {@link Protos.PaymentRequest} object may
|
||||
* be fetched in the r= parameter.
|
||||
* If verifyPki is specified and the payment request object specifies a PKI method, then the system trust store will
|
||||
* be used to verify the signature provided by the payment request. An exception is thrown by the future if the
|
||||
* signature cannot be verified.
|
||||
* If trustStorePath is not null, the trust store used for PKI verification will be loaded from the given location
|
||||
* instead of using the system default trust store location.
|
||||
*/
|
||||
public static ListenableFuture<PaymentSession> createFromBitcoinUri(final BitcoinURI uri, final boolean verifyPki, @Nullable final String trustStorePath)
|
||||
throws PaymentRequestException {
|
||||
String url = uri.getPaymentRequestUrl();
|
||||
if (url == null)
|
||||
throw new PaymentRequestException.InvalidPaymentRequestURL("No payment request URL (r= parameter) in BitcoinURI " + uri);
|
||||
try {
|
||||
return fetchPaymentRequest(new URI(url), verifyPki, trustStorePath);
|
||||
} catch (URISyntaxException e) {
|
||||
throw new PaymentRequestException.InvalidPaymentRequestURL(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a future that will be notified with a PaymentSession object after it is fetched using the provided url.
|
||||
* url is an address where the {@link Protos.PaymentRequest} object may be fetched.
|
||||
* If verifyPki is specified and the payment request object specifies a PKI method, then the system trust store will
|
||||
* be used to verify the signature provided by the payment request. An exception is thrown by the future if the
|
||||
* signature cannot be verified.
|
||||
*/
|
||||
public static ListenableFuture<PaymentSession> createFromUrl(final String url) throws PaymentRequestException {
|
||||
return createFromUrl(url, true, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a future that will be notified with a PaymentSession object after it is fetched using the provided url.
|
||||
* url is an address where the {@link Protos.PaymentRequest} object may be fetched.
|
||||
* If the payment request object specifies a PKI method, then the system trust store will
|
||||
* be used to verify the signature provided by the payment request. An exception is thrown by the future if the
|
||||
* signature cannot be verified.
|
||||
*/
|
||||
public static ListenableFuture<PaymentSession> createFromUrl(final String url, final boolean verifyPki)
|
||||
throws PaymentRequestException {
|
||||
return createFromUrl(url, verifyPki, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a future that will be notified with a PaymentSession object after it is fetched using the provided url.
|
||||
* url is an address where the {@link Protos.PaymentRequest} object may be fetched.
|
||||
* If the payment request object specifies a PKI method, then the system trust store will
|
||||
* be used to verify the signature provided by the payment request. An exception is thrown by the future if the
|
||||
* signature cannot be verified.
|
||||
* If trustStorePath is not null, the trust store used for PKI verification will be loaded from the given location
|
||||
* instead of using the system default trust store location.
|
||||
*/
|
||||
public static ListenableFuture<PaymentSession> createFromUrl(final String url, final boolean verifyPki, @Nullable final String trustStorePath)
|
||||
throws PaymentRequestException {
|
||||
if (url == null)
|
||||
throw new PaymentRequestException.InvalidPaymentRequestURL("null paymentRequestUrl");
|
||||
try {
|
||||
return fetchPaymentRequest(new URI(url), true, trustStorePath);
|
||||
} catch(URISyntaxException e) {
|
||||
throw new PaymentRequestException.InvalidPaymentRequestURL(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static ListenableFuture<PaymentSession> fetchPaymentRequest(final URI uri, final boolean verifyPki, @Nullable final String trustStorePath) {
|
||||
ListeningExecutorService executor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
|
||||
return executor.submit(new Callable<PaymentSession>() {
|
||||
@Override
|
||||
public PaymentSession call() throws Exception {
|
||||
HttpURLConnection connection = (HttpURLConnection)uri.toURL().openConnection();
|
||||
connection.setRequestProperty("Accept", "application/bitcoin-paymentrequest");
|
||||
connection.setUseCaches(false);
|
||||
Protos.PaymentRequest paymentRequest = Protos.PaymentRequest.parseFrom(connection.getInputStream());
|
||||
PaymentSession paymentSession = new PaymentSession(paymentRequest, verifyPki, trustStorePath);
|
||||
return paymentSession;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a PaymentSession from the provided {@link Protos.PaymentRequest}.
|
||||
* Verifies PKI by default.
|
||||
*/
|
||||
public PaymentSession(Protos.PaymentRequest request) throws PaymentRequestException {
|
||||
parsePaymentRequest(request);
|
||||
verifyPki();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a PaymentSession from the provided {@link Protos.PaymentRequest}.
|
||||
* If verifyPki is true, also validates the signature and throws an exception if it fails.
|
||||
*/
|
||||
public PaymentSession(Protos.PaymentRequest request, boolean verifyPki) throws PaymentRequestException {
|
||||
parsePaymentRequest(request);
|
||||
if (verifyPki)
|
||||
verifyPki();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a PaymentSession from the provided {@link Protos.PaymentRequest}.
|
||||
* If verifyPki is true, also validates the signature and throws an exception if it fails.
|
||||
* If trustStorePath is not null, the trust store used for PKI verification will be loaded from the given location
|
||||
* instead of using the system default trust store location.
|
||||
*/
|
||||
public PaymentSession(Protos.PaymentRequest request, boolean verifyPki, @Nullable final String trustStorePath) throws PaymentRequestException {
|
||||
this.trustStorePath = trustStorePath;
|
||||
parsePaymentRequest(request);
|
||||
if (verifyPki)
|
||||
verifyPki();
|
||||
}
|
||||
|
||||
/**
|
||||
* Message returned by the merchant in response to a Payment message.
|
||||
*/
|
||||
public class Ack {
|
||||
private String memo = "";
|
||||
|
||||
Ack(String memo) {
|
||||
this.memo = memo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the memo included by the merchant in the payment ack. This message is typically displayed to the user
|
||||
* as a notification (e.g. "Your payment was received and is being processed").
|
||||
*/
|
||||
public String getMemo() {
|
||||
return memo;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the memo included by the merchant in the payment request.
|
||||
*/
|
||||
public String getMemo() {
|
||||
return paymentDetails.getMemo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total amount of bitcoins requested.
|
||||
*/
|
||||
public BigInteger getValue() {
|
||||
return totalValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the date that the payment request was generated.
|
||||
*/
|
||||
public Date getDate() {
|
||||
return new Date(paymentDetails.getTime() * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* This should always be called before attempting to call sendPayment.
|
||||
*/
|
||||
public boolean isExpired() {
|
||||
return paymentDetails.hasExpires() && System.currentTimeMillis() / 1000L > paymentDetails.getExpires();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the payment url where the Payment message should be sent.
|
||||
* Returns null if no payment url was provided in the PaymentRequest.
|
||||
*/
|
||||
public @Nullable String getPaymentUrl() {
|
||||
if (paymentDetails.hasPaymentUrl())
|
||||
return paymentDetails.getPaymentUrl();
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Wallet.SendRequest} suitable for broadcasting to the network.
|
||||
*/
|
||||
public Wallet.SendRequest getSendRequest() {
|
||||
Transaction tx = new Transaction(params);
|
||||
for (Protos.Output output : paymentDetails.getOutputsList())
|
||||
tx.addOutput(new TransactionOutput(params, tx, BigInteger.valueOf(output.getAmount()), output.getScript().toByteArray()));
|
||||
return Wallet.SendRequest.forTx(tx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a Payment message and sends the payment to the merchant who sent the PaymentRequest.
|
||||
* Provide transactions built by the wallet.
|
||||
* NOTE: This does not broadcast the transactions to the bitcoin network, it merely sends a Payment message to the
|
||||
* merchant confirming the payment.
|
||||
* Returns an object wrapping PaymentACK once received.
|
||||
* If the PaymentRequest did not specify a payment_url, returns null and does nothing.
|
||||
* @param txns list of transactions to be included with the Payment message.
|
||||
* @param refundAddr will be used by the merchant to send money back if there was a problem.
|
||||
* @param memo is a message to include in the payment message sent to the merchant.
|
||||
*/
|
||||
public @Nullable ListenableFuture<Ack> sendPayment(List<Transaction> txns, @Nullable Address refundAddr, @Nullable String memo)
|
||||
throws PaymentRequestException, VerificationException, IOException {
|
||||
Protos.Payment payment = getPayment(txns, refundAddr, memo);
|
||||
if (payment == null)
|
||||
return null;
|
||||
if (isExpired())
|
||||
throw new PaymentRequestException.Expired("PaymentRequest is expired");
|
||||
URL url;
|
||||
try {
|
||||
url = new URL(paymentDetails.getPaymentUrl());
|
||||
} catch (MalformedURLException e) {
|
||||
throw new PaymentRequestException.InvalidPaymentURL(e);
|
||||
}
|
||||
return sendPayment(url, payment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a Payment message based on the information in the PaymentRequest.
|
||||
* Provide transactions built by the wallet.
|
||||
* If the PaymentRequest did not specify a payment_url, returns null.
|
||||
* @param txns list of transactions to be included with the Payment message.
|
||||
* @param refundAddr will be used by the merchant to send money back if there was a problem.
|
||||
* @param memo is a message to include in the payment message sent to the merchant.
|
||||
*/
|
||||
public @Nullable Protos.Payment getPayment(List<Transaction> txns, @Nullable Address refundAddr, @Nullable String memo)
|
||||
throws IOException {
|
||||
if (!paymentDetails.hasPaymentUrl())
|
||||
return null;
|
||||
Protos.Payment.Builder payment = Protos.Payment.newBuilder();
|
||||
if (paymentDetails.hasMerchantData())
|
||||
payment.setMerchantData(paymentDetails.getMerchantData());
|
||||
if (refundAddr != null) {
|
||||
Protos.Output.Builder refundOutput = Protos.Output.newBuilder();
|
||||
refundOutput.setAmount(totalValue.longValue());
|
||||
refundOutput.setScript(ByteString.copyFrom(ScriptBuilder.createOutputScript(refundAddr).getProgram()));
|
||||
payment.addRefundTo(refundOutput);
|
||||
}
|
||||
if (memo != null) {
|
||||
payment.setMemo(memo);
|
||||
}
|
||||
for (Transaction txn : txns) {
|
||||
txn.verify();
|
||||
ByteArrayOutputStream o = new ByteArrayOutputStream();
|
||||
txn.bitcoinSerialize(o);
|
||||
payment.addTransactions(ByteString.copyFrom(o.toByteArray()));
|
||||
}
|
||||
return payment.build();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
protected ListenableFuture<Ack> sendPayment(final URL url, final Protos.Payment payment) {
|
||||
return executor.submit(new Callable<Ack>() {
|
||||
@Override
|
||||
public Ack call() throws Exception {
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setRequestMethod("POST");
|
||||
connection.setRequestProperty("Content-Type", "application/bitcoin-payment");
|
||||
connection.setRequestProperty("Accept", "application/bitcoin-paymentack");
|
||||
connection.setRequestProperty("Content-Length", Integer.toString(payment.getSerializedSize()));
|
||||
connection.setUseCaches(false);
|
||||
connection.setDoInput(true);
|
||||
connection.setDoOutput(true);
|
||||
|
||||
// Send request.
|
||||
DataOutputStream outStream = new DataOutputStream(connection.getOutputStream());
|
||||
payment.writeTo(outStream);
|
||||
outStream.flush();
|
||||
outStream.close();
|
||||
|
||||
// Get response.
|
||||
InputStream inStream = connection.getInputStream();
|
||||
Protos.PaymentACK.Builder paymentAckBuilder = Protos.PaymentACK.newBuilder().mergeFrom(inStream);
|
||||
Protos.PaymentACK paymentAck = paymentAckBuilder.build();
|
||||
String memo = "";
|
||||
if (paymentAck.hasMemo())
|
||||
memo = paymentAck.getMemo();
|
||||
return new Ack(memo);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about the X509 signature's issuer and subject.
|
||||
*/
|
||||
public static class PkiVerificationData {
|
||||
public String name;
|
||||
public PublicKey merchantSigningKey;
|
||||
public TrustAnchor rootAuthority;
|
||||
public String orgName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the provided PKI method to find the corresponding public key and verify the provided signature.
|
||||
* Returns null if no PKI method was specified in the {@link Protos.PaymentRequest}.
|
||||
*/
|
||||
public @Nullable PkiVerificationData verifyPki() throws PaymentRequestException {
|
||||
try {
|
||||
if (pkiVerificationData != null)
|
||||
return pkiVerificationData;
|
||||
if (paymentRequest.getPkiType().equals("none"))
|
||||
// Nothing to verify. Everything is fine. Move along.
|
||||
return null;
|
||||
|
||||
String algorithm;
|
||||
if (paymentRequest.getPkiType().equals("x509+sha256"))
|
||||
algorithm = "SHA256withRSA";
|
||||
else if (paymentRequest.getPkiType().equals("x509+sha1"))
|
||||
algorithm = "SHA1withRSA";
|
||||
else
|
||||
throw new PaymentRequestException.InvalidPkiType("Unsupported PKI type: " + paymentRequest.getPkiType());
|
||||
|
||||
Protos.X509Certificates protoCerts = Protos.X509Certificates.parseFrom(paymentRequest.getPkiData());
|
||||
if (protoCerts.getCertificateCount() == 0)
|
||||
throw new PaymentRequestException.InvalidPkiData("No certificates provided in message: server config error");
|
||||
|
||||
// Parse the certs and turn into a certificate chain object. Cert factories can parse both DER and base64.
|
||||
// The ordering of certificates is defined by the payment protocol spec to be the same as what the Java
|
||||
// crypto API requires - convenient!
|
||||
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
|
||||
List<X509Certificate> certs = Lists.newArrayList();
|
||||
for (ByteString bytes : protoCerts.getCertificateList())
|
||||
certs.add((X509Certificate) certificateFactory.generateCertificate(bytes.newInput()));
|
||||
CertPath path = certificateFactory.generateCertPath(certs);
|
||||
|
||||
// Retrieves the most-trusted CAs from keystore.
|
||||
PKIXParameters params = new PKIXParameters(createKeyStore(trustStorePath));
|
||||
// Revocation not supported in the current version.
|
||||
params.setRevocationEnabled(false);
|
||||
|
||||
// Now verify the certificate chain is correct and trusted. This let's us get an identity linked pubkey.
|
||||
CertPathValidator validator = CertPathValidator.getInstance("PKIX");
|
||||
PKIXCertPathValidatorResult result = (PKIXCertPathValidatorResult) validator.validate(path, params);
|
||||
PublicKey publicKey = result.getPublicKey();
|
||||
// OK, we got an identity, now check it was used to sign this message.
|
||||
Signature signature = Signature.getInstance(algorithm);
|
||||
// Note that we don't use signature.initVerify(certs.get(0)) here despite it being the most obvious
|
||||
// way to set it up, because we don't care about the constraints specified on the certificates: any
|
||||
// cert that links a key to a domain name or other identity will do for us.
|
||||
signature.initVerify(publicKey);
|
||||
Protos.PaymentRequest.Builder reqToCheck = paymentRequest.toBuilder();
|
||||
reqToCheck.setSignature(ByteString.EMPTY);
|
||||
signature.update(reqToCheck.build().toByteArray());
|
||||
if (!signature.verify(paymentRequest.getSignature().toByteArray()))
|
||||
throw new PaymentRequestException.PkiVerificationException("Invalid signature, this payment request is not valid.");
|
||||
|
||||
// Signature verifies, get the names from the identity we just verified for presentation to the user.
|
||||
X500Principal principal = certs.get(0).getSubjectX500Principal();
|
||||
// At this point the Java crypto API falls flat on its face and dies - there's no clean way to get the
|
||||
// different parts of the certificate name except for parsing the string. That's hard because of various
|
||||
// custom escaping rules and the usual crap. So, use Bouncy Castle to re-parse the string into binary form
|
||||
// again and then look for the names we want. Fail!
|
||||
org.spongycastle.asn1.x500.X500Name name = new X500Name(principal.getName());
|
||||
String entityName = null, orgName = null;
|
||||
for (RDN rdn : name.getRDNs()) {
|
||||
AttributeTypeAndValue pair = rdn.getFirst();
|
||||
if (pair.getType().equals(RFC4519Style.cn))
|
||||
entityName = ((ASN1String)pair.getValue()).getString();
|
||||
else if (pair.getType().equals(RFC4519Style.o))
|
||||
orgName = ((ASN1String)pair.getValue()).getString();
|
||||
}
|
||||
|
||||
// Everything is peachy. Return some useful data to the caller.
|
||||
PkiVerificationData data = new PkiVerificationData();
|
||||
data.name = entityName;
|
||||
data.orgName = orgName;
|
||||
data.merchantSigningKey = publicKey;
|
||||
data.rootAuthority = result.getTrustAnchor();
|
||||
// Cache the result so we don't have to re-verify if this method is called again.
|
||||
pkiVerificationData = data;
|
||||
return data;
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
// Data structures are malformed.
|
||||
throw new PaymentRequestException.InvalidPkiData(e);
|
||||
} catch (CertificateException e) {
|
||||
// The X.509 certificate data didn't parse correctly.
|
||||
throw new PaymentRequestException.PkiVerificationException(e);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
// Should never happen so don't make users have to think about it. PKIX is always present.
|
||||
throw new RuntimeException(e);
|
||||
} catch (InvalidAlgorithmParameterException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (CertPathValidatorException e) {
|
||||
// The certificate chain isn't known or trusted, probably, the server is using an SSL root we don't
|
||||
// know about and the user needs to upgrade to a new version of the software (or import a root cert).
|
||||
throw new PaymentRequestException.PkiVerificationException(e);
|
||||
} catch (InvalidKeyException e) {
|
||||
// Shouldn't happen if the certs verified correctly.
|
||||
throw new PaymentRequestException.PkiVerificationException(e);
|
||||
} catch (SignatureException e) {
|
||||
// Something went wrong during hashing (yes, despite the name, this does not mean the sig was invalid).
|
||||
throw new PaymentRequestException.PkiVerificationException(e);
|
||||
} catch (IOException e) {
|
||||
throw new PaymentRequestException.PkiVerificationException(e);
|
||||
} catch (KeyStoreException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private KeyStore createKeyStore(@Nullable String path)
|
||||
throws IOException, KeyStoreException, NoSuchAlgorithmException, CertificateException {
|
||||
String keyStoreType = KeyStore.getDefaultType();
|
||||
char[] defaultPassword = "changeit".toCharArray();
|
||||
if (path != null) {
|
||||
// If the user provided path, only try to load the keystore at that path.
|
||||
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
|
||||
FileInputStream is = new FileInputStream(path);
|
||||
keyStore.load(is, defaultPassword);
|
||||
return keyStore;
|
||||
}
|
||||
path = System.getProperty("javax.net.ssl.trustStore");
|
||||
if (path == null) {
|
||||
// Check if we are on Android.
|
||||
try {
|
||||
Class Build = Class.forName("android.os.Build");
|
||||
Object version = Build.getDeclaredField("VERSION").get(Build);
|
||||
// Build.VERSION_CODES.ICE_CREAM_SANDWICH is 14.
|
||||
if (version.getClass().getDeclaredField("SDK_INT").getInt(version) >= 14) {
|
||||
// After ICS, Android provided this nice method for loading the keystore,
|
||||
// so we don't have to specify the location explicitly.
|
||||
KeyStore keystore = KeyStore.getInstance("AndroidCAStore");
|
||||
keystore.load(null, null);
|
||||
return keystore;
|
||||
} else {
|
||||
keyStoreType = "BKS";
|
||||
path = System.getProperty("java.home") + "/etc/security/cacerts.bks".replace('/', File.separatorChar);
|
||||
}
|
||||
} catch (ClassNotFoundException e) {
|
||||
// NOP. android.os.Build is not present, so we are not on Android.
|
||||
} catch (NoSuchFieldException e) {
|
||||
// This should never happen.
|
||||
} catch (IllegalAccessException e) {
|
||||
// This should never happen.
|
||||
}
|
||||
}
|
||||
if (path == null) {
|
||||
// We are not on Android. Try this default system location for Linux/Windows/OSX.
|
||||
path = System.getProperty("java.home") + "/lib/security/cacerts".replace('/', File.separatorChar);
|
||||
}
|
||||
try {
|
||||
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
|
||||
FileInputStream is = new FileInputStream(path);
|
||||
keyStore.load(is, defaultPassword);
|
||||
return keyStore;
|
||||
} catch (FileNotFoundException e) {
|
||||
// If we failed to find a system trust store, load our own fallback trust store.
|
||||
KeyStore keyStore = KeyStore.getInstance("JKS");
|
||||
InputStream is = getClass().getResourceAsStream("cacerts");
|
||||
keyStore.load(is, defaultPassword);
|
||||
return keyStore;
|
||||
}
|
||||
}
|
||||
|
||||
private void parsePaymentRequest(Protos.PaymentRequest request) throws PaymentRequestException {
|
||||
try {
|
||||
if (request == null)
|
||||
throw new PaymentRequestException("request cannot be null");
|
||||
if (!request.hasPaymentDetailsVersion())
|
||||
throw new PaymentRequestException.InvalidVersion("No version");
|
||||
if (request.getPaymentDetailsVersion() != 1)
|
||||
throw new PaymentRequestException.InvalidVersion("Version 1 required. Received version " + request.getPaymentDetailsVersion());
|
||||
paymentRequest = request;
|
||||
if (!request.hasSerializedPaymentDetails())
|
||||
throw new PaymentRequestException("No PaymentDetails");
|
||||
paymentDetails = Protos.PaymentDetails.newBuilder().mergeFrom(request.getSerializedPaymentDetails()).build();
|
||||
if (paymentDetails == null)
|
||||
throw new PaymentRequestException("Invalid PaymentDetails");
|
||||
if (!paymentDetails.hasNetwork())
|
||||
params = MainNetParams.get();
|
||||
else
|
||||
params = NetworkParameters.fromPmtProtocolID(paymentDetails.getNetwork());
|
||||
if (params == null)
|
||||
throw new PaymentRequestException.InvalidNetwork("Invalid network " + paymentDetails.getNetwork());
|
||||
if (paymentDetails.getOutputsCount() < 1)
|
||||
throw new PaymentRequestException.InvalidOutputs("No outputs");
|
||||
for (Protos.Output output : paymentDetails.getOutputsList()) {
|
||||
if (output.hasAmount())
|
||||
totalValue = totalValue.add(BigInteger.valueOf(output.getAmount()));
|
||||
}
|
||||
// This won't ever happen in practice. It would only happen if the user provided outputs
|
||||
// that are obviously invalid. Still, we don't want to silently overflow.
|
||||
if (totalValue.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0)
|
||||
throw new PaymentRequestException.InvalidOutputs("The outputs are way too big.");
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
throw new PaymentRequestException(e);
|
||||
}
|
||||
}
|
||||
}
|
@ -86,6 +86,7 @@ public class BitcoinURI {
|
||||
public static final String FIELD_LABEL = "label";
|
||||
public static final String FIELD_AMOUNT = "amount";
|
||||
public static final String FIELD_ADDRESS = "address";
|
||||
public static final String FIELD_PAYMENT_REQUEST_URL = "r";
|
||||
|
||||
public static final String BITCOIN_SCHEME = "bitcoin";
|
||||
private static final String ENCODED_SPACE_CHARACTER = "%20";
|
||||
@ -268,6 +269,14 @@ public class BitcoinURI {
|
||||
public String getMessage() {
|
||||
return (String) parameterMap.get(FIELD_MESSAGE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The URL where a payment request (as specified in BIP 70) may
|
||||
* be fetched.
|
||||
*/
|
||||
public String getPaymentRequestUrl() {
|
||||
return (String) parameterMap.get(FIELD_PAYMENT_REQUEST_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name The name of the parameter
|
||||
|
6022
core/src/main/java/org/bitcoin/protocols/payments/Protos.java
Normal file
6022
core/src/main/java/org/bitcoin/protocols/payments/Protos.java
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
70
core/src/paymentrequest.proto
Normal file
70
core/src/paymentrequest.proto
Normal file
@ -0,0 +1,70 @@
|
||||
/** 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Authors: Mike Hearn, Gavin Andresen
|
||||
*/
|
||||
|
||||
/* Notes:
|
||||
* - Endianness: All byte arrays that represent numbers (such as hashes and private keys) are Big Endian
|
||||
* - To regenerate after editing, run mvn clean package -DupdateProtobuf
|
||||
*/
|
||||
|
||||
//
|
||||
// Simple Bitcoin Payment Protocol messages
|
||||
//
|
||||
// Use fields 100+ for extensions;
|
||||
// to avoid conflicts, register extensions at:
|
||||
// https://en.bitcoin.it/wiki/Payment_Request
|
||||
//
|
||||
|
||||
package payments;
|
||||
option java_package = "org.bitcoin.protocols.payments";
|
||||
option java_outer_classname = "Protos";
|
||||
|
||||
// Generalized form of "send payment to this/these bitcoin addresses"
|
||||
message Output {
|
||||
optional uint64 amount = 1 [default = 0]; // amount is integer-number-of-satoshis
|
||||
required bytes script = 2; // usually one of the standard Script forms
|
||||
}
|
||||
message PaymentDetails {
|
||||
optional string network = 1 [default = "main"]; // "main" or "test"
|
||||
repeated Output outputs = 2; // Where payment should be sent
|
||||
required uint64 time = 3; // Timestamp; when payment request created
|
||||
optional uint64 expires = 4; // Timestamp; when this request should be considered invalid
|
||||
optional string memo = 5; // Human-readable description of request for the customer
|
||||
optional string payment_url = 6; // URL to send Payment and get PaymentACK
|
||||
optional bytes merchant_data = 7; // Arbitrary data to include in the Payment message
|
||||
}
|
||||
message PaymentRequest {
|
||||
optional uint32 payment_details_version = 1 [default = 1];
|
||||
optional string pki_type = 2 [default = "none"]; // none / x509+sha256 / x509+sha1
|
||||
optional bytes pki_data = 3; // depends on pki_type
|
||||
required bytes serialized_payment_details = 4; // PaymentDetails
|
||||
optional bytes signature = 5; // pki-dependent signature
|
||||
}
|
||||
message X509Certificates {
|
||||
repeated bytes certificate = 1; // DER-encoded X.509 certificate chain
|
||||
}
|
||||
message Payment {
|
||||
optional bytes merchant_data = 1; // From PaymentDetails.merchant_data
|
||||
repeated bytes transactions = 2; // Signed transactions that satisfy PaymentDetails.outputs
|
||||
repeated Output refund_to = 3; // Where to send refunds, if a refund is necessary
|
||||
optional string memo = 4; // Human-readable message for the merchant
|
||||
}
|
||||
message PaymentACK {
|
||||
required Payment payment = 1; // Payment message that triggered this ACK
|
||||
optional string memo = 2; // human-readable message for customer
|
||||
}
|
@ -0,0 +1,191 @@
|
||||
/**
|
||||
* 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.bitcoin.protocols.payments;
|
||||
|
||||
import com.google.bitcoin.core.*;
|
||||
import com.google.bitcoin.params.TestNet3Params;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.protobuf.ByteString;
|
||||
import org.bitcoin.protocols.payments.Protos;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class PaymentSessionTest {
|
||||
private static final Logger log = LoggerFactory.getLogger(PaymentSessionTest.class);
|
||||
private static final NetworkParameters params = TestNet3Params.get();
|
||||
private static final String simplePaymentUrl = "http://a.simple.url.com/";
|
||||
private static final String paymentRequestMemo = "send coinz noa plz kthx";
|
||||
private static final String paymentMemo = "take ze coinz";
|
||||
private static final ByteString merchantData = ByteString.copyFromUtf8("merchant data");
|
||||
private static final long time = System.currentTimeMillis() / 1000L;
|
||||
private ECKey serverKey;
|
||||
private Transaction tx;
|
||||
private TransactionOutput outputToMe;
|
||||
BigInteger nanoCoins = Utils.toNanoCoins(1, 0);
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
serverKey = new ECKey();
|
||||
tx = new Transaction(params);
|
||||
outputToMe = new TransactionOutput(params, tx, nanoCoins, serverKey);
|
||||
tx.addOutput(outputToMe);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSimplePayment() throws Exception {
|
||||
// Create a PaymentRequest and make sure the correct values are parsed by the PaymentSession.
|
||||
MockPaymentSession paymentSession = new MockPaymentSession(newSimplePaymentRequest());
|
||||
assertEquals(paymentRequestMemo, paymentSession.getMemo());
|
||||
assertEquals(nanoCoins, paymentSession.getValue());
|
||||
assertEquals(simplePaymentUrl, paymentSession.getPaymentUrl());
|
||||
assertTrue(new Date(time * 1000L).equals(paymentSession.getDate()));
|
||||
assertTrue(paymentSession.getSendRequest().tx.equals(tx));
|
||||
assertFalse(paymentSession.isExpired());
|
||||
|
||||
// Send the payment and verify that the correct information is sent.
|
||||
// Add a dummy input to tx so it is considered valid.
|
||||
tx.addInput(new TransactionInput(params, tx, outputToMe.getScriptBytes()));
|
||||
ArrayList<Transaction> txns = new ArrayList<Transaction>();
|
||||
txns.add(tx);
|
||||
Address refundAddr = new Address(params, serverKey.getPubKeyHash());
|
||||
paymentSession.sendPayment(txns, refundAddr, paymentMemo);
|
||||
assertEquals(1, paymentSession.getPaymentLog().size());
|
||||
assertEquals(simplePaymentUrl, paymentSession.getPaymentLog().get(0).getUrl().toString());
|
||||
Protos.Payment payment = paymentSession.getPaymentLog().get(0).getPayment();
|
||||
assertEquals(paymentMemo, payment.getMemo());
|
||||
assertEquals(merchantData, payment.getMerchantData());
|
||||
assertEquals(1, payment.getRefundToCount());
|
||||
assertEquals(nanoCoins.longValue(), payment.getRefundTo(0).getAmount());
|
||||
TransactionOutput refundOutput = new TransactionOutput(params, null, nanoCoins, refundAddr);
|
||||
ByteString refundScript = ByteString.copyFrom(refundOutput.getScriptBytes());
|
||||
assertTrue(refundScript.equals(payment.getRefundTo(0).getScript()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExpiredPaymentRequest() throws Exception {
|
||||
MockPaymentSession paymentSession = new MockPaymentSession(newExpiredPaymentRequest());
|
||||
assertTrue(paymentSession.isExpired());
|
||||
// Send the payment and verify that an exception is thrown.
|
||||
// Add a dummy input to tx so it is considered valid.
|
||||
tx.addInput(new TransactionInput(params, tx, outputToMe.getScriptBytes()));
|
||||
ArrayList<Transaction> txns = new ArrayList<Transaction>();
|
||||
txns.add(tx);
|
||||
try {
|
||||
paymentSession.sendPayment(txns, null, null);
|
||||
} catch(PaymentRequestException.Expired e) {
|
||||
assertEquals(0, paymentSession.getPaymentLog().size());
|
||||
assertEquals(e.getMessage(), "PaymentRequest is expired");
|
||||
return;
|
||||
}
|
||||
fail("Expected exception due to expired PaymentRequest");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPkiVerification() throws Exception {
|
||||
InputStream in = getClass().getResourceAsStream("pki_test.bitcoinpaymentrequest");
|
||||
Protos.PaymentRequest paymentRequest = Protos.PaymentRequest.newBuilder().mergeFrom(in).build();
|
||||
MockPaymentSession paymentSession = new MockPaymentSession(paymentRequest);
|
||||
PaymentSession.PkiVerificationData pkiData = paymentSession.verifyPki();
|
||||
assertEquals("www.bitcoincore.org", pkiData.name);
|
||||
}
|
||||
|
||||
private Protos.PaymentRequest newSimplePaymentRequest() {
|
||||
Protos.Output.Builder outputBuilder = Protos.Output.newBuilder()
|
||||
.setAmount(nanoCoins.longValue())
|
||||
.setScript(ByteString.copyFrom(outputToMe.getScriptBytes()));
|
||||
Protos.PaymentDetails paymentDetails = Protos.PaymentDetails.newBuilder()
|
||||
.setNetwork("test")
|
||||
.setTime(time)
|
||||
.setPaymentUrl(simplePaymentUrl)
|
||||
.addOutputs(outputBuilder)
|
||||
.setMemo(paymentRequestMemo)
|
||||
.setMerchantData(merchantData)
|
||||
.build();
|
||||
Protos.PaymentRequest paymentRequest = Protos.PaymentRequest.newBuilder()
|
||||
.setPaymentDetailsVersion(1)
|
||||
.setPkiType("none")
|
||||
.setSerializedPaymentDetails(paymentDetails.toByteString())
|
||||
.build();
|
||||
return paymentRequest;
|
||||
}
|
||||
|
||||
private Protos.PaymentRequest newExpiredPaymentRequest() {
|
||||
Protos.Output.Builder outputBuilder = Protos.Output.newBuilder()
|
||||
.setAmount(nanoCoins.longValue())
|
||||
.setScript(ByteString.copyFrom(outputToMe.getScriptBytes()));
|
||||
Protos.PaymentDetails paymentDetails = Protos.PaymentDetails.newBuilder()
|
||||
.setNetwork("test")
|
||||
.setTime(time - 10)
|
||||
.setExpires(time - 1)
|
||||
.setPaymentUrl(simplePaymentUrl)
|
||||
.addOutputs(outputBuilder)
|
||||
.setMemo(paymentRequestMemo)
|
||||
.setMerchantData(merchantData)
|
||||
.build();
|
||||
Protos.PaymentRequest paymentRequest = Protos.PaymentRequest.newBuilder()
|
||||
.setPaymentDetailsVersion(1)
|
||||
.setPkiType("none")
|
||||
.setSerializedPaymentDetails(paymentDetails.toByteString())
|
||||
.build();
|
||||
return paymentRequest ;
|
||||
}
|
||||
|
||||
private class MockPaymentSession extends PaymentSession {
|
||||
private ArrayList<PaymentLogItem> paymentLog = new ArrayList<PaymentLogItem>();
|
||||
|
||||
public MockPaymentSession(Protos.PaymentRequest request) throws PaymentRequestException {
|
||||
super(request);
|
||||
}
|
||||
|
||||
public ArrayList<PaymentLogItem> getPaymentLog() {
|
||||
return paymentLog;
|
||||
}
|
||||
|
||||
protected ListenableFuture<Ack> sendPayment(final URL url, final Protos.Payment payment) {
|
||||
paymentLog.add(new PaymentLogItem(url, payment));
|
||||
return null;
|
||||
}
|
||||
|
||||
public class PaymentLogItem {
|
||||
private final URL url;
|
||||
private final Protos.Payment payment;
|
||||
|
||||
PaymentLogItem(final URL url, final Protos.Payment payment) {
|
||||
this.url = url;
|
||||
this.payment = payment;
|
||||
}
|
||||
|
||||
public URL getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public Protos.Payment getPayment() {
|
||||
return payment;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
@ -23,10 +23,18 @@ import com.google.bitcoin.net.discovery.PeerDiscovery;
|
||||
import com.google.bitcoin.params.MainNetParams;
|
||||
import com.google.bitcoin.params.RegTestParams;
|
||||
import com.google.bitcoin.params.TestNet3Params;
|
||||
import com.google.bitcoin.protocols.payments.PaymentRequestException;
|
||||
import com.google.bitcoin.protocols.payments.PaymentSession;
|
||||
import com.google.bitcoin.store.*;
|
||||
import com.google.bitcoin.uri.BitcoinURI;
|
||||
import com.google.bitcoin.uri.BitcoinURIParseException;
|
||||
import com.google.bitcoin.utils.BriefLogFormatter;
|
||||
import com.google.common.base.Charsets;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.io.Resources;
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import joptsimple.OptionParser;
|
||||
import joptsimple.OptionSet;
|
||||
import joptsimple.OptionSpec;
|
||||
@ -72,6 +80,7 @@ public class WalletTool {
|
||||
private static PeerDiscovery discovery;
|
||||
private static ValidationMode mode;
|
||||
private static String password;
|
||||
private static org.bitcoin.protocols.payments.Protos.PaymentRequest paymentRequest;
|
||||
|
||||
public static class Condition {
|
||||
public enum Type {
|
||||
@ -199,6 +208,8 @@ public class WalletTool {
|
||||
parser.accepts("offline");
|
||||
parser.accepts("ignore-mandatory-extensions");
|
||||
OptionSpec<String> passwordFlag = parser.accepts("password").withRequiredArg();
|
||||
OptionSpec<String> paymentRequestLocation = parser.accepts("payment-request").withRequiredArg();
|
||||
parser.accepts("no-pki");
|
||||
options = parser.parse(args);
|
||||
|
||||
final String HELP_TEXT = Resources.toString(WalletTool.class.getResource("wallet-tool-help.txt"), Charsets.UTF_8);
|
||||
@ -305,20 +316,26 @@ public class WalletTool {
|
||||
case RESET: reset(); break;
|
||||
case SYNC: syncChain(); break;
|
||||
case SEND:
|
||||
if (!options.has(outputFlag)) {
|
||||
System.err.println("You must specify at least one --output=addr:value.");
|
||||
if (options.has(paymentRequestLocation) && options.has(outputFlag)) {
|
||||
System.err.println("--payment-request and --output cannot be used together.");
|
||||
return;
|
||||
} else if (options.has(outputFlag)) {
|
||||
BigInteger fee = BigInteger.ZERO;
|
||||
if (options.has("fee")) {
|
||||
fee = Utils.toNanoCoins((String)options.valueOf("fee"));
|
||||
}
|
||||
String lockTime = null;
|
||||
if (options.has("locktime")) {
|
||||
lockTime = (String) options.valueOf("locktime");
|
||||
}
|
||||
boolean allowUnconfirmed = options.has("allow-unconfirmed");
|
||||
send(outputFlag.values(options), fee, lockTime, allowUnconfirmed);
|
||||
} else if (options.has(paymentRequestLocation)) {
|
||||
sendPaymentRequest(paymentRequestLocation.value(options), !options.has("no-pki"));
|
||||
} else {
|
||||
System.err.println("You must specify a --payment-request or at least one --output=addr:value.");
|
||||
return;
|
||||
}
|
||||
BigInteger fee = BigInteger.ZERO;
|
||||
if (options.has("fee")) {
|
||||
fee = Utils.toNanoCoins((String)options.valueOf("fee"));
|
||||
}
|
||||
String lockTime = null;
|
||||
if (options.has("locktime")) {
|
||||
lockTime = (String) options.valueOf("locktime");
|
||||
}
|
||||
boolean allowUnconfirmed = options.has("allow-unconfirmed");
|
||||
send(outputFlag.values(options), fee, lockTime, allowUnconfirmed);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -461,6 +478,85 @@ public class WalletTool {
|
||||
}
|
||||
}
|
||||
|
||||
private static void sendPaymentRequest(String location, boolean verifyPki) {
|
||||
if (location.startsWith("http")) {
|
||||
try {
|
||||
ListenableFuture<PaymentSession> future = PaymentSession.createFromUrl(location, verifyPki);
|
||||
Futures.addCallback(future, new FutureCallback<PaymentSession>() {
|
||||
@Override
|
||||
public void onSuccess(PaymentSession session) {
|
||||
if (session != null)
|
||||
send(session);
|
||||
else {
|
||||
System.err.println("Server returned null session");
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
public void onFailure(Throwable thrown) {
|
||||
System.err.println("Failed to fetch payment request " + thrown.getMessage());
|
||||
System.exit(1);
|
||||
}
|
||||
});
|
||||
} catch (PaymentRequestException e) {
|
||||
System.err.println("Error creating payment session " + e.getMessage());
|
||||
System.exit(1);
|
||||
}
|
||||
} else if (location.startsWith("bitcoin")) {
|
||||
BitcoinURI paymentRequestURI = null;
|
||||
try {
|
||||
paymentRequestURI = new BitcoinURI(location);
|
||||
} catch (BitcoinURIParseException e) {
|
||||
System.err.println("Invalid bitcoin uri " + e.getMessage());
|
||||
System.exit(1);
|
||||
}
|
||||
try {
|
||||
ListenableFuture<PaymentSession> future = PaymentSession.createFromBitcoinUri(paymentRequestURI, verifyPki);
|
||||
Futures.addCallback(future, new FutureCallback<PaymentSession>() {
|
||||
@Override
|
||||
public void onSuccess(PaymentSession session) {
|
||||
if (session != null)
|
||||
send(session);
|
||||
else {
|
||||
System.err.println("Server returned null session");
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
public void onFailure(Throwable thrown) {
|
||||
System.err.println("Failed to fetch payment request " + thrown.getMessage());
|
||||
System.exit(1);
|
||||
}
|
||||
});
|
||||
} catch (PaymentRequestException e) {
|
||||
System.err.println("Error creating payment session " + e.getMessage());
|
||||
System.exit(1);
|
||||
}
|
||||
} else {
|
||||
// Try to open the payment request as a file.
|
||||
FileInputStream stream = null;
|
||||
try {
|
||||
File paymentRequestFile = new File(location);
|
||||
stream = new FileInputStream(paymentRequestFile);
|
||||
} catch (Exception e) {
|
||||
System.err.println("Failed to open file " + e.getMessage());
|
||||
System.exit(1);
|
||||
}
|
||||
try {
|
||||
paymentRequest = org.bitcoin.protocols.payments.Protos.PaymentRequest.newBuilder().mergeFrom(stream).build();
|
||||
} catch(IOException e) {
|
||||
System.err.println("Failed to parse payment request from file " + e.getMessage());
|
||||
System.exit(1);
|
||||
}
|
||||
PaymentSession session = null;
|
||||
try {
|
||||
session = new PaymentSession(paymentRequest, verifyPki);
|
||||
} catch (PaymentRequestException e) {
|
||||
System.err.println("Error creating payment session " + e.getMessage());
|
||||
System.exit(1);
|
||||
}
|
||||
send(session);
|
||||
}
|
||||
}
|
||||
|
||||
private static void wait(WaitForEnum waitFor) throws BlockStoreException {
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
setup();
|
||||
@ -737,4 +833,49 @@ public class WalletTool {
|
||||
setup();
|
||||
System.out.println(wallet.toString(true, true, true, chain));
|
||||
}
|
||||
|
||||
private static void send(PaymentSession session) {
|
||||
try {
|
||||
System.out.println("Payment Request");
|
||||
System.out.println("Amount: " + session.getValue().doubleValue() / 100000 + "mBTC");
|
||||
System.out.println("Date: " + session.getDate());
|
||||
System.out.println("Memo: " + session.getMemo());
|
||||
if (session.pkiVerificationData != null) {
|
||||
System.out.println("Pki-Verified Name: " + session.pkiVerificationData.name);
|
||||
if (session.pkiVerificationData.orgName != null)
|
||||
System.out.println("Pki-Verified Org: " + session.pkiVerificationData.orgName);
|
||||
}
|
||||
final Wallet.SendRequest req = session.getSendRequest();
|
||||
wallet.completeTx(req); // may throw InsufficientMoneyException.
|
||||
// No refund address specified, no user-specified memo field.
|
||||
ListenableFuture<PaymentSession.Ack> future = session.sendPayment(ImmutableList.of(req.tx), null, null);
|
||||
Futures.addCallback(future, new FutureCallback<PaymentSession.Ack>() {
|
||||
@Override
|
||||
public void onSuccess(PaymentSession.Ack ack) {
|
||||
try {
|
||||
wallet.commitTx(req.tx);
|
||||
System.out.println(ack.getMemo());
|
||||
} catch (VerificationException e) {
|
||||
System.err.println("Failed to send tx " + e.getMessage());
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
public void onFailure(Throwable thrown) {
|
||||
System.err.println("Failed to send payment " + thrown.getMessage());
|
||||
System.exit(1);
|
||||
}
|
||||
});
|
||||
}catch (PaymentRequestException e) {
|
||||
System.err.println("Failed to send payment " + e.getMessage());
|
||||
System.exit(1);
|
||||
} catch (VerificationException e) {
|
||||
System.err.println("Failed to send payment " + e.getMessage());
|
||||
System.exit(1);
|
||||
} catch (IOException e) {
|
||||
System.err.println("Invalid payment " + e.getMessage());
|
||||
System.exit(1);
|
||||
} catch (InsufficientMoneyException e) {
|
||||
System.err.println("Insufficient funds: have " + Utils.bitcoinValueToFriendlyString(wallet.getBalance()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,18 +19,28 @@ Usage: wallet-tool --flags action-name
|
||||
sync Sync the wallet with the latest block chain (download new transactions).
|
||||
If the chain file does not exist this will RESET the wallet.
|
||||
reset Deletes all transactions from the wallet, for if you want to replay the chain.
|
||||
send Creates a transaction with the given --output from this wallet and broadcasts, eg:
|
||||
send Creates and broadcasts a transaction from the given wallet.
|
||||
Requires either --output or --payment-request to be specified.
|
||||
If --output is specified, a transaction is created from the provided output
|
||||
from this wallet and broadcasted, eg:
|
||||
--output=1GthXFQMktFLWdh5EPNGqbq3H6WdG8zsWj:1.245
|
||||
You can repeat --output=address:value multiple times.
|
||||
If the output destination starts with 04 and is 65 or 33 bytes long it will be
|
||||
treated as a public key instead of an address and the send will use
|
||||
<key> CHECKSIG as the script.
|
||||
If --payment-request is specified, a transaction will be created using the provided
|
||||
payment request. A payment request can be a local file, a bitcoin uri, or url to
|
||||
download the payment request, e.g.:
|
||||
--payment-request=/path/to/my.bitcoinpaymentrequest
|
||||
--payment-request=bitcoin:?r=http://merchant.com/pay.php?123
|
||||
--payment-request=http://merchant.com/pay.php?123
|
||||
|
||||
Other options include:
|
||||
--fee=0.01 sets the tx fee
|
||||
--locktime=1234 sets the lock time to block 1234
|
||||
--locktime=2013/01/01 sets the lock time to 1st Jan 2013
|
||||
--allow-unconfirmed will let you create spends of pending non-change outputs.
|
||||
|
||||
--no-pki disables pki verification for payment requests.
|
||||
|
||||
>>> GENERAL OPTIONS
|
||||
--debuglog Enables logging from the core library.
|
||||
|
Loading…
Reference in New Issue
Block a user