3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-02-11 17:55:53 +00:00

Add Dogecoin payment protocol support

Add Dogecoin payment protocol support, and remove unused proto definition
files.
This commit is contained in:
Ross Nicoll 2015-08-08 22:47:46 +01:00
parent 049fa77002
commit 39fe02389b
4 changed files with 964 additions and 0 deletions

View File

@ -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;
/**
* <p>Utility methods and constants for working with <a href="https://github.com/bitcoin/bips/blob/master/bip-0070.mediawiki">
* BIP 70 aka the payment protocol</a>. 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.</p>
*
* <p>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.</p>
*/
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<Protos.Output> 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<X509Certificate> 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<Transaction> 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<Transaction> transactions,
@Nullable List<Protos.Output> 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<Transaction> parseTransactionsFromPaymentMessage(NetworkParameters params,
Protos.Payment paymentMessage) {
final List<Transaction> transactions = new ArrayList<Transaction>(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;
}
}
}

View File

@ -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<X509Certificate> certificates;
public PkiVerificationException(String msg) {
super(msg);
}
public PkiVerificationException(Exception e) {
super(e);
}
public PkiVerificationException(Exception e, List<X509Certificate> certificates) {
super(e);
this.certificates = certificates;
}
}
}

View File

@ -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;
/**
* <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>
*
* <p>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.</p>
*
* <p>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.</p>
*
* <p>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.</p>
*
* @see <a href="https://github.com/bitcoin/bips/blob/master/bip-0070.mediawiki">BIP 0070</a>
*/
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;
/**
* <p>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.</p>
*
* <p>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.</p>
*/
public static ListenableFuture<PaymentSession> 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<PaymentSession> 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<PaymentSession> 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<PaymentSession> 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<PaymentSession> 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<PaymentSession> 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<PaymentSession> fetchPaymentRequest(final URI uri, final boolean verifyPki, @Nullable final TrustStoreLoader trustStoreLoader) {
return executor.submit(new Callable<PaymentSession>() {
@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<PaymentProtocol.Output> getOutputs() {
List<PaymentProtocol.Output> outputs = new ArrayList<PaymentProtocol.Output>(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<PaymentProtocol.Ack> sendPayment(List<Transaction> 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<Transaction> 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<PaymentProtocol.Ack> sendPayment(final URL url, final Protos.Payment payment) {
return executor.submit(new Callable<PaymentProtocol.Ack>() {
@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;
}
}

View File

@ -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;