From 39fe02389b87f11d9a0e39c9b91507ae3eacb7ce Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Sat, 8 Aug 2015 22:47:46 +0100 Subject: [PATCH] Add Dogecoin payment protocol support Add Dogecoin payment protocol support, and remove unused proto definition files. --- .../protocols/payments/PaymentProtocol.java | 423 +++++++++++++++++ .../payments/PaymentProtocolException.java | 107 +++++ .../protocols/payments/PaymentSession.java | 429 ++++++++++++++++++ .../protocols/payments/package-info.java | 5 + 4 files changed, 964 insertions(+) create mode 100644 src/main/java/com/dogecoin/dogecoinj/protocols/payments/PaymentProtocol.java create mode 100644 src/main/java/com/dogecoin/dogecoinj/protocols/payments/PaymentProtocolException.java create mode 100644 src/main/java/com/dogecoin/dogecoinj/protocols/payments/PaymentSession.java create mode 100644 src/main/java/com/dogecoin/dogecoinj/protocols/payments/package-info.java diff --git a/src/main/java/com/dogecoin/dogecoinj/protocols/payments/PaymentProtocol.java b/src/main/java/com/dogecoin/dogecoinj/protocols/payments/PaymentProtocol.java new file mode 100644 index 00000000..c26eef00 --- /dev/null +++ b/src/main/java/com/dogecoin/dogecoinj/protocols/payments/PaymentProtocol.java @@ -0,0 +1,423 @@ +/** + * 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 com.dogecoin.dogecoinj.protocols.payments; + +import org.bitcoinj.core.*; +import org.bitcoinj.crypto.X509Utils; +import org.bitcoinj.script.ScriptBuilder; +import com.google.common.base.Objects; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import org.bitcoin.protocols.payments.Protos; + +import javax.annotation.Nullable; +import java.io.Serializable; +import java.security.*; +import java.security.cert.*; +import java.security.cert.Certificate; +import java.util.ArrayList; +import java.util.List; + +/** + *

Utility methods and constants for working with + * BIP 70 aka the payment protocol. These are low level wrappers around the protocol buffers. If you're implementing + * a wallet app, look at {@link PaymentSession} for a higher level API that should simplify working with the protocol.

+ * + *

BIP 70 defines a binary, protobuf based protocol that runs directly between sender and receiver of funds. Payment + * protocol data does not flow over the Bitcoin P2P network or enter the block chain. It's instead for data that is only + * of interest to the parties involved but isn't otherwise needed for consensus.

+ */ +public class PaymentProtocol { + + // MIME types as defined in DIP71. + public static final String MIMETYPE_PAYMENTREQUEST = "application/vnd.doge.payment.request"; + public static final String MIMETYPE_PAYMENT = "application/vnd.doge.payment.payment"; + public static final String MIMETYPE_PAYMENTACK = "application/vnd.doge.payment.ack"; + + /** + * Create a payment request with one standard pay to address output. You may want to sign the request using + * {@link #signPaymentRequest}. Use {@link Protos.PaymentRequest.Builder#build} to get the actual payment + * request. + * + * @param params network parameters + * @param amount amount of coins to request, or null + * @param toAddress address to request coins to + * @param memo arbitrary, user readable memo, or null if none + * @param paymentUrl URL to send payment message to, or null if none + * @param merchantData arbitrary merchant data, or null if none + * @return created payment request, in its builder form + */ + public static Protos.PaymentRequest.Builder createPaymentRequest(NetworkParameters params, + @Nullable Coin amount, Address toAddress, @Nullable String memo, @Nullable String paymentUrl, + @Nullable byte[] merchantData) { + return createPaymentRequest(params, ImmutableList.of(createPayToAddressOutput(amount, toAddress)), memo, + paymentUrl, merchantData); + } + + /** + * Create a payment request. You may want to sign the request using {@link #signPaymentRequest}. Use + * {@link Protos.PaymentRequest.Builder#build} to get the actual payment request. + * + * @param params network parameters + * @param outputs list of outputs to request coins to + * @param memo arbitrary, user readable memo, or null if none + * @param paymentUrl URL to send payment message to, or null if none + * @param merchantData arbitrary merchant data, or null if none + * @return created payment request, in its builder form + */ + public static Protos.PaymentRequest.Builder createPaymentRequest(NetworkParameters params, + List outputs, @Nullable String memo, @Nullable String paymentUrl, + @Nullable byte[] merchantData) { + final Protos.PaymentDetails.Builder paymentDetails = Protos.PaymentDetails.newBuilder(); + paymentDetails.setNetwork(params.getPaymentProtocolId()); + for (Protos.Output output : outputs) + paymentDetails.addOutputs(output); + if (memo != null) + paymentDetails.setMemo(memo); + if (paymentUrl != null) + paymentDetails.setPaymentUrl(paymentUrl); + if (merchantData != null) + paymentDetails.setMerchantData(ByteString.copyFrom(merchantData)); + paymentDetails.setTime(Utils.currentTimeSeconds()); + + final Protos.PaymentRequest.Builder paymentRequest = Protos.PaymentRequest.newBuilder(); + paymentRequest.setSerializedPaymentDetails(paymentDetails.build().toByteString()); + return paymentRequest; + } + + /** + * Parse a payment request. + * + * @param paymentRequest payment request to parse + * @return instance of {@link PaymentSession}, used as a value object + * @throws PaymentProtocolException + */ + public static PaymentSession parsePaymentRequest(Protos.PaymentRequest paymentRequest) + throws PaymentProtocolException { + return new PaymentSession(paymentRequest, false, null); + } + + /** + * Sign the provided payment request. + * + * @param paymentRequest Payment request to sign, in its builder form. + * @param certificateChain Certificate chain to send with the payment request, ordered from client certificate to root + * certificate. The root certificate itself may be omitted. + * @param privateKey The key to sign with. Must match the public key from the first certificate of the certificate chain. + */ + public static void signPaymentRequest(Protos.PaymentRequest.Builder paymentRequest, + X509Certificate[] certificateChain, PrivateKey privateKey) { + try { + final Protos.X509Certificates.Builder certificates = Protos.X509Certificates.newBuilder(); + for (final Certificate certificate : certificateChain) + certificates.addCertificate(ByteString.copyFrom(certificate.getEncoded())); + + paymentRequest.setPkiType("x509+sha256"); + paymentRequest.setPkiData(certificates.build().toByteString()); + paymentRequest.setSignature(ByteString.EMPTY); + final Protos.PaymentRequest paymentRequestToSign = paymentRequest.build(); + + final String algorithm; + if (privateKey.getAlgorithm().equalsIgnoreCase("RSA")) + algorithm = "SHA256withRSA"; + else + throw new IllegalStateException(privateKey.getAlgorithm()); + + final Signature signature = Signature.getInstance(algorithm); + signature.initSign(privateKey); + signature.update(paymentRequestToSign.toByteArray()); + + paymentRequest.setSignature(ByteString.copyFrom(signature.sign())); + } catch (final GeneralSecurityException x) { + // Should never happen so don't make users have to think about it. + throw new RuntimeException(x); + } + } + + /** + * Uses the provided PKI method to find the corresponding public key and verify the provided signature. + * + * @param paymentRequest Payment request to verify. + * @param trustStore KeyStore of trusted root certificate authorities. + * @return verification data, or null if no PKI method was specified in the {@link Protos.PaymentRequest}. + * @throws PaymentProtocolException if payment request could not be verified. + */ + public static @Nullable PkiVerificationData verifyPaymentRequestPki(Protos.PaymentRequest paymentRequest, KeyStore trustStore) + throws PaymentProtocolException { + List certs = null; + try { + final String pkiType = paymentRequest.getPkiType(); + if (pkiType.equals("none")) + // Nothing to verify. Everything is fine. Move along. + return null; + + String algorithm; + if (pkiType.equals("x509+sha256")) + algorithm = "SHA256withRSA"; + else if (pkiType.equals("x509+sha1")) + algorithm = "SHA1withRSA"; + else + throw new PaymentProtocolException.InvalidPkiType("Unsupported PKI type: " + pkiType); + + Protos.X509Certificates protoCerts = Protos.X509Certificates.parseFrom(paymentRequest.getPkiData()); + if (protoCerts.getCertificateCount() == 0) + throw new PaymentProtocolException.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"); + 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(trustStore); + // 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 PaymentProtocolException.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. + final X509Certificate cert = certs.get(0); + String displayName = X509Utils.getDisplayNameFromCertificate(cert, true); + if (displayName == null) + throw new PaymentProtocolException.PkiVerificationException("Could not extract name from certificate"); + // Everything is peachy. Return some useful data to the caller. + return new PkiVerificationData(displayName, publicKey, result.getTrustAnchor()); + } catch (InvalidProtocolBufferException e) { + // Data structures are malformed. + throw new PaymentProtocolException.InvalidPkiData(e); + } catch (CertificateException e) { + // The X.509 certificate data didn't parse correctly. + throw new PaymentProtocolException.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 PaymentProtocolException.PkiVerificationException(e, certs); + } catch (InvalidKeyException e) { + // Shouldn't happen if the certs verified correctly. + throw new PaymentProtocolException.PkiVerificationException(e); + } catch (SignatureException e) { + // Something went wrong during hashing (yes, despite the name, this does not mean the sig was invalid). + throw new PaymentProtocolException.PkiVerificationException(e); + } catch (KeyStoreException e) { + throw new RuntimeException(e); + } + } + + /** + * Information about the X.509 signature's issuer and subject. + */ + public static class PkiVerificationData { + /** Display name of the payment requestor, could be a domain name, email address, legal name, etc */ + public final String displayName; + /** SSL public key that was used to sign. */ + public final PublicKey merchantSigningKey; + /** Object representing the CA that verified the merchant's ID */ + public final TrustAnchor rootAuthority; + /** String representing the display name of the CA that verified the merchant's ID */ + public final String rootAuthorityName; + + private PkiVerificationData(@Nullable String displayName, PublicKey merchantSigningKey, + TrustAnchor rootAuthority) throws PaymentProtocolException.PkiVerificationException { + try { + this.displayName = displayName; + this.merchantSigningKey = merchantSigningKey; + this.rootAuthority = rootAuthority; + this.rootAuthorityName = X509Utils.getDisplayNameFromCertificate(rootAuthority.getTrustedCert(), true); + } catch (CertificateParsingException x) { + throw new PaymentProtocolException.PkiVerificationException(x); + } + } + + @Override + public String toString() { + return Objects.toStringHelper(this) + .add("displayName", displayName) + .add("rootAuthorityName", rootAuthorityName) + .add("merchantSigningKey", merchantSigningKey) + .add("rootAuthority", rootAuthority) + .toString(); + } + } + + /** + * Create a payment message with one standard pay to address output. + * + * @param transactions one or more transactions that satisfy the requested outputs. + * @param refundAmount amount of coins to request as a refund, or null if no refund. + * @param refundAddress address to refund coins to + * @param memo arbitrary, user readable memo, or null if none + * @param merchantData arbitrary merchant data, or null if none + * @return created payment message + */ + public static Protos.Payment createPaymentMessage(List transactions, + @Nullable Coin refundAmount, @Nullable Address refundAddress, @Nullable String memo, + @Nullable byte[] merchantData) { + if (refundAddress != null) { + if (refundAmount == null) + throw new IllegalArgumentException("Specify refund amount if refund address is specified."); + return createPaymentMessage(transactions, + ImmutableList.of(createPayToAddressOutput(refundAmount, refundAddress)), memo, merchantData); + } else { + return createPaymentMessage(transactions, null, memo, merchantData); + } + } + + /** + * Create a payment message. This wraps up transaction data along with anything else useful for making a payment. + * + * @param transactions transactions to include with the payment message + * @param refundOutputs list of outputs to refund coins to, or null + * @param memo arbitrary, user readable memo, or null if none + * @param merchantData arbitrary merchant data, or null if none + * @return created payment message + */ + public static Protos.Payment createPaymentMessage(List transactions, + @Nullable List refundOutputs, @Nullable String memo, @Nullable byte[] merchantData) { + Protos.Payment.Builder builder = Protos.Payment.newBuilder(); + for (Transaction transaction : transactions) { + transaction.verify(); + builder.addTransactions(ByteString.copyFrom(transaction.unsafeBitcoinSerialize())); + } + if (refundOutputs != null) { + for (Protos.Output output : refundOutputs) + builder.addRefundTo(output); + } + if (memo != null) + builder.setMemo(memo); + if (merchantData != null) + builder.setMerchantData(ByteString.copyFrom(merchantData)); + return builder.build(); + } + + /** + * Parse transactions from payment message. + * + * @param params network parameters (needed for transaction deserialization) + * @param paymentMessage payment message to parse + * @return list of transactions + */ + public static List parseTransactionsFromPaymentMessage(NetworkParameters params, + Protos.Payment paymentMessage) { + final List transactions = new ArrayList(paymentMessage.getTransactionsCount()); + for (final ByteString transaction : paymentMessage.getTransactionsList()) + transactions.add(new Transaction(params, transaction.toByteArray())); + return transactions; + } + + /** + * Message returned by the merchant in response to a Payment message. + */ + public static class Ack { + @Nullable private final String memo; + + Ack(@Nullable 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"). If none was provided, returns + * null. + */ + @Nullable public String getMemo() { + return memo; + } + } + + /** + * Create a payment ack. + * + * @param paymentMessage payment message to send with the ack + * @param memo arbitrary, user readable memo, or null if none + * @return created payment ack + */ + public static Protos.PaymentACK createPaymentAck(Protos.Payment paymentMessage, @Nullable String memo) { + final Protos.PaymentACK.Builder builder = Protos.PaymentACK.newBuilder(); + builder.setPayment(paymentMessage); + if (memo != null) + builder.setMemo(memo); + return builder.build(); + } + + /** + * Parse payment ack into an object. + */ + public static Ack parsePaymentAck(Protos.PaymentACK paymentAck) { + final String memo = paymentAck.hasMemo() ? paymentAck.getMemo() : null; + return new Ack(memo); + } + + /** + * Create a standard pay to address output for usage in {@link #createPaymentRequest} and + * {@link #createPaymentMessage}. + * + * @param amount amount to pay, or null + * @param address address to pay to + * @return output + */ + public static Protos.Output createPayToAddressOutput(@Nullable Coin amount, Address address) { + Protos.Output.Builder output = Protos.Output.newBuilder(); + if (amount != null) { + if (amount.compareTo(NetworkParameters.MAX_MONEY) > 0) + throw new IllegalArgumentException("Amount too big: " + amount); + output.setAmount(amount.value); + } else { + output.setAmount(0); + } + output.setScript(ByteString.copyFrom(ScriptBuilder.createOutputScript(address).getProgram())); + return output.build(); + } + + /** + * Value object to hold amount/script pairs. + */ + public static class Output implements Serializable { + public final @Nullable Coin amount; + public final byte[] scriptData; + + public Output(@Nullable Coin amount, byte[] scriptData) { + this.amount = amount; + this.scriptData = scriptData; + } + } +} diff --git a/src/main/java/com/dogecoin/dogecoinj/protocols/payments/PaymentProtocolException.java b/src/main/java/com/dogecoin/dogecoinj/protocols/payments/PaymentProtocolException.java new file mode 100644 index 00000000..9ed8d7de --- /dev/null +++ b/src/main/java/com/dogecoin/dogecoinj/protocols/payments/PaymentProtocolException.java @@ -0,0 +1,107 @@ +/** + * 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.dogecoin.dogecoinj.protocols.payments; + +import java.security.cert.X509Certificate; +import java.util.List; + +public class PaymentProtocolException extends Exception { + public PaymentProtocolException(String msg) { + super(msg); + } + + public PaymentProtocolException(Exception e) { + super(e); + } + + public static class Expired extends PaymentProtocolException { + public Expired(String msg) { + super(msg); + } + } + + public static class InvalidPaymentRequestURL extends PaymentProtocolException { + public InvalidPaymentRequestURL(String msg) { + super(msg); + } + + public InvalidPaymentRequestURL(Exception e) { + super(e); + } + } + + public static class InvalidPaymentURL extends PaymentProtocolException { + public InvalidPaymentURL(Exception e) { + super(e); + } + + public InvalidPaymentURL(String msg) { + super(msg); + } + } + + public static class InvalidOutputs extends PaymentProtocolException { + public InvalidOutputs(String msg) { + super(msg); + } + } + + public static class InvalidVersion extends PaymentProtocolException { + public InvalidVersion(String msg) { + super(msg); + } + } + + public static class InvalidNetwork extends PaymentProtocolException { + public InvalidNetwork(String msg) { + super(msg); + } + } + + public static class InvalidPkiType extends PaymentProtocolException { + public InvalidPkiType(String msg) { + super(msg); + } + } + + public static class InvalidPkiData extends PaymentProtocolException { + public InvalidPkiData(String msg) { + super(msg); + } + + public InvalidPkiData(Exception e) { + super(e); + } + } + + public static class PkiVerificationException extends PaymentProtocolException { + public List certificates; + + public PkiVerificationException(String msg) { + super(msg); + } + + public PkiVerificationException(Exception e) { + super(e); + } + + public PkiVerificationException(Exception e, List certificates) { + super(e); + this.certificates = certificates; + } + } +} diff --git a/src/main/java/com/dogecoin/dogecoinj/protocols/payments/PaymentSession.java b/src/main/java/com/dogecoin/dogecoinj/protocols/payments/PaymentSession.java new file mode 100644 index 00000000..c3789624 --- /dev/null +++ b/src/main/java/com/dogecoin/dogecoinj/protocols/payments/PaymentSession.java @@ -0,0 +1,429 @@ +/** + * 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.dogecoin.dogecoinj.protocols.payments; + +import com.dogecoin.dogecoinj.protocols.payments.PaymentProtocol.PkiVerificationData; +import org.bitcoinj.core.*; +import org.bitcoinj.crypto.TrustStoreLoader; +import org.bitcoinj.params.MainNetParams; +import org.bitcoinj.uri.BitcoinURI; +import org.bitcoinj.utils.Threading; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.protobuf.InvalidProtocolBufferException; + +import org.bitcoin.protocols.payments.Protos; + +import javax.annotation.Nullable; + +import java.io.*; +import java.net.*; +import java.security.KeyStoreException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.concurrent.Callable; + +/** + *

Provides a standard implementation of the Payment Protocol (BIP 0070)

+ * + *

A PaymentSession can be initialized from one of the following:

+ * + *
    + *
  • A {@link BitcoinURI} object that conforms to BIP 0072
  • + *
  • A url where the {@link Protos.PaymentRequest} can be fetched
  • + *
  • Directly with a {@link Protos.PaymentRequest} object
  • + *
+ * + *

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. Instead it 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, and then broadcast the + * tx itself later if needed.

+ * + * @see BIP 0070 + */ +public class PaymentSession { + private static ListeningExecutorService executor = Threading.THREAD_POOL; + private NetworkParameters params; + private final TrustStoreLoader trustStoreLoader; + private Protos.PaymentRequest paymentRequest; + private Protos.PaymentDetails paymentDetails; + private Coin totalValue = Coin.ZERO; + + /** + * Stores the calculated PKI verification data, or null if none is available. + * Only valid after the session is created with the verifyPki parameter set to true. + */ + @Nullable public final 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 createFromBitcoinUri(final BitcoinURI uri) throws PaymentProtocolException { + 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 createFromBitcoinUri(final BitcoinURI uri, final boolean verifyPki) + throws PaymentProtocolException { + 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 trustStoreLoader is null, the system default trust store is used. + */ + public static ListenableFuture createFromBitcoinUri(final BitcoinURI uri, final boolean verifyPki, @Nullable final TrustStoreLoader trustStoreLoader) + throws PaymentProtocolException { + String url = uri.getPaymentRequestUrl(); + if (url == null) + throw new PaymentProtocolException.InvalidPaymentRequestURL("No payment request URL (r= parameter) in BitcoinURI " + uri); + try { + return fetchPaymentRequest(new URI(url), verifyPki, trustStoreLoader); + } catch (URISyntaxException e) { + throw new PaymentProtocolException.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 createFromUrl(final String url) throws PaymentProtocolException { + 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 createFromUrl(final String url, final boolean verifyPki) + throws PaymentProtocolException { + 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 trustStoreLoader is null, the system default trust store is used. + */ + public static ListenableFuture createFromUrl(final String url, final boolean verifyPki, @Nullable final TrustStoreLoader trustStoreLoader) + throws PaymentProtocolException { + if (url == null) + throw new PaymentProtocolException.InvalidPaymentRequestURL("null paymentRequestUrl"); + try { + return fetchPaymentRequest(new URI(url), verifyPki, trustStoreLoader); + } catch(URISyntaxException e) { + throw new PaymentProtocolException.InvalidPaymentRequestURL(e); + } + } + + private static ListenableFuture fetchPaymentRequest(final URI uri, final boolean verifyPki, @Nullable final TrustStoreLoader trustStoreLoader) { + return executor.submit(new Callable() { + @Override + public PaymentSession call() throws Exception { + HttpURLConnection connection = (HttpURLConnection)uri.toURL().openConnection(); + connection.setRequestProperty("Accept", PaymentProtocol.MIMETYPE_PAYMENTREQUEST); + connection.setUseCaches(false); + Protos.PaymentRequest paymentRequest = Protos.PaymentRequest.parseFrom(connection.getInputStream()); + return new PaymentSession(paymentRequest, verifyPki, trustStoreLoader); + } + }); + } + + /** + * Creates a PaymentSession from the provided {@link Protos.PaymentRequest}. + * Verifies PKI by default. + */ + public PaymentSession(Protos.PaymentRequest request) throws PaymentProtocolException { + this(request, true, null); + } + + /** + * 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 PaymentProtocolException { + this(request, verifyPki, null); + } + + /** + * 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 trustStoreLoader is null, the system default trust store is used. + */ + public PaymentSession(Protos.PaymentRequest request, boolean verifyPki, @Nullable final TrustStoreLoader trustStoreLoader) throws PaymentProtocolException { + this.trustStoreLoader = trustStoreLoader != null ? trustStoreLoader : new TrustStoreLoader.DefaultTrustStoreLoader(); + parsePaymentRequest(request); + if (verifyPki) { + try { + pkiVerificationData = PaymentProtocol.verifyPaymentRequestPki(request, this.trustStoreLoader.getKeyStore()); + } catch (IOException x) { + throw new PaymentProtocolException(x); + } catch (KeyStoreException x) { + throw new PaymentProtocolException(x); + } + } else { + pkiVerificationData = null; + } + } + + /** + * Returns the outputs of the payment request. + */ + public List getOutputs() { + List outputs = new ArrayList(paymentDetails.getOutputsCount()); + for (Protos.Output output : paymentDetails.getOutputsList()) { + Coin amount = output.hasAmount() ? Coin.valueOf(output.getAmount()) : null; + outputs.add(new PaymentProtocol.Output(amount, output.getScript().toByteArray())); + } + return outputs; + } + + /** + * Returns the memo included by the merchant in the payment request, or null if not found. + */ + @Nullable public String getMemo() { + if (paymentDetails.hasMemo()) + return paymentDetails.getMemo(); + else + return null; + } + + /** + * Returns the total amount of bitcoins requested. + */ + public Coin getValue() { + return totalValue; + } + + /** + * Returns the date that the payment request was generated. + */ + public Date getDate() { + return new Date(paymentDetails.getTime() * 1000); + } + + /** + * Returns the expires time of the payment request, or null if none. + */ + @Nullable public Date getExpires() { + if (paymentDetails.hasExpires()) + return new Date(paymentDetails.getExpires() * 1000); + else + return null; + } + + /** + * 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 the merchant data included by the merchant in the payment request, or null if none. + */ + @Nullable public byte[] getMerchantData() { + if (paymentDetails.hasMerchantData()) + return paymentDetails.getMerchantData().toByteArray(); + else + 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, Coin.valueOf(output.getAmount()), output.getScript().toByteArray())); + return Wallet.SendRequest.forTx(tx).fromPaymentDetails(paymentDetails); + } + + /** + * 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 sendPayment(List txns, @Nullable Address refundAddr, @Nullable String memo) + throws PaymentProtocolException, VerificationException, IOException { + Protos.Payment payment = getPayment(txns, refundAddr, memo); + if (payment == null) + return null; + if (isExpired()) + throw new PaymentProtocolException.Expired("PaymentRequest is expired"); + URL url; + try { + url = new URL(paymentDetails.getPaymentUrl()); + } catch (MalformedURLException e) { + throw new PaymentProtocolException.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 txns, @Nullable Address refundAddr, @Nullable String memo) + throws IOException, PaymentProtocolException.InvalidNetwork { + if (paymentDetails.hasPaymentUrl()) { + for (Transaction tx : txns) + if (!tx.getParams().equals(params)) + throw new PaymentProtocolException.InvalidNetwork(params.getPaymentProtocolId()); + return PaymentProtocol.createPaymentMessage(txns, totalValue, refundAddr, memo, getMerchantData()); + } else { + return null; + } + } + + @VisibleForTesting + protected ListenableFuture sendPayment(final URL url, final Protos.Payment payment) { + return executor.submit(new Callable() { + @Override + public PaymentProtocol.Ack call() throws Exception { + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", PaymentProtocol.MIMETYPE_PAYMENT); + connection.setRequestProperty("Accept", PaymentProtocol.MIMETYPE_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. + Protos.PaymentACK paymentAck = Protos.PaymentACK.parseFrom(connection.getInputStream()); + return PaymentProtocol.parsePaymentAck(paymentAck); + } + }); + } + + private void parsePaymentRequest(Protos.PaymentRequest request) throws PaymentProtocolException { + try { + if (request == null) + throw new PaymentProtocolException("request cannot be null"); + if (request.getPaymentDetailsVersion() != 1) + throw new PaymentProtocolException.InvalidVersion("Version 1 required. Received version " + request.getPaymentDetailsVersion()); + paymentRequest = request; + if (!request.hasSerializedPaymentDetails()) + throw new PaymentProtocolException("No PaymentDetails"); + paymentDetails = Protos.PaymentDetails.newBuilder().mergeFrom(request.getSerializedPaymentDetails()).build(); + if (paymentDetails == null) + throw new PaymentProtocolException("Invalid PaymentDetails"); + if (!paymentDetails.hasNetwork()) + params = MainNetParams.get(); + else + params = NetworkParameters.fromPmtProtocolID(paymentDetails.getNetwork()); + if (params == null) + throw new PaymentProtocolException.InvalidNetwork("Invalid network " + paymentDetails.getNetwork()); + if (paymentDetails.getOutputsCount() < 1) + throw new PaymentProtocolException.InvalidOutputs("No outputs"); + for (Protos.Output output : paymentDetails.getOutputsList()) { + if (output.hasAmount()) + totalValue = totalValue.add(Coin.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(NetworkParameters.MAX_MONEY) > 0) + throw new PaymentProtocolException.InvalidOutputs("The outputs are way too big."); + } catch (InvalidProtocolBufferException e) { + throw new PaymentProtocolException(e); + } + } + + /** Returns the value of pkiVerificationData or null if it wasn't verified at construction time. */ + @Nullable public PkiVerificationData verifyPki() { + return pkiVerificationData; + } + + /** Gets the params as read from the PaymentRequest.network field: main is the default if missing. */ + public NetworkParameters getNetworkParameters() { + return params; + } + + /** Returns the protobuf that this object was instantiated with. */ + public Protos.PaymentRequest getPaymentRequest() { + return paymentRequest; + } + + /** Returns the protobuf that describes the payment to be made. */ + public Protos.PaymentDetails getPaymentDetails() { + return paymentDetails; + } +} diff --git a/src/main/java/com/dogecoin/dogecoinj/protocols/payments/package-info.java b/src/main/java/com/dogecoin/dogecoinj/protocols/payments/package-info.java new file mode 100644 index 00000000..fb419735 --- /dev/null +++ b/src/main/java/com/dogecoin/dogecoinj/protocols/payments/package-info.java @@ -0,0 +1,5 @@ +/** + * The BIP70 payment protocol wraps Bitcoin transactions and adds various useful features like memos, refund addresses + * and authentication. + */ +package com.dogecoin.dogecoinj.protocols.payments; \ No newline at end of file