/**
* 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.google.bitcoin.protocols.payments;
import com.google.bitcoin.core.*;
import com.google.bitcoin.params.MainNetParams;
import com.google.bitcoin.script.ScriptBuilder;
import com.google.bitcoin.uri.BitcoinURI;
import com.google.bitcoin.utils.Threading;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import org.bitcoin.protocols.payments.Protos;
import org.spongycastle.asn1.ASN1String;
import org.spongycastle.asn1.x500.AttributeTypeAndValue;
import org.spongycastle.asn1.x500.RDN;
import org.spongycastle.asn1.x500.X500Name;
import org.spongycastle.asn1.x500.style.RFC4519Style;
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;
/**
* <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
* @author Andreas Schildbach
* @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 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), verifyPki, trustStorePath);
} catch(URISyntaxException e) {
throw new PaymentRequestException.InvalidPaymentRequestURL(e);
}
}
private static ListenableFuture<PaymentSession> fetchPaymentRequest(final URI uri, final boolean verifyPki, @Nullable final String trustStorePath) {
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());
return new PaymentSession(paymentRequest, verifyPki, trustStorePath);
}
});
}
/**
* 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 {
@Nullable private 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;
}
}
/**
* 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 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 = null;
if (paymentAck.hasMemo())
memo = paymentAck.getMemo();
return new Ack(memo);
}
});
}
/**
* Information about the X509 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 name;
/** The "org" part of the payment requestors ID. */
public final String orgName;
/** 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 name, @Nullable String orgName, PublicKey merchantSigningKey,
TrustAnchor rootAuthority) throws PaymentRequestException.PkiVerificationException {
this.name = name;
this.orgName = orgName;
this.merchantSigningKey = merchantSigningKey;
this.rootAuthority = rootAuthority;
this.rootAuthorityName = getNameFromCert(rootAuthority);
}
private @Nullable String getNameFromCert(TrustAnchor rootAuthority) throws PaymentRequestException.PkiVerificationException {
org.spongycastle.asn1.x500.X500Name name = new X500Name(rootAuthority.getTrustedCert().getSubjectX500Principal().getName());
String commonName = null, org = null, location = null, country = null;
for (RDN rdn : name.getRDNs()) {
AttributeTypeAndValue pair = rdn.getFirst();
String val = ((ASN1String)pair.getValue()).getString();
if (pair.getType().equals(RFC4519Style.cn))
commonName = val;
else if (pair.getType().equals(RFC4519Style.o))
org = val;
else if (pair.getType().equals(RFC4519Style.l))
location = val;
else if (pair.getType().equals(RFC4519Style.c))
country = val;
}
if (org != null) {
return Joiner.on(", ").skipNulls().join(org, location, country);
} else {
return commonName;
}
}
}
/**
* 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();
}
if (entityName == null && orgName == null)
throw new PaymentRequestException.PkiVerificationException("Invalid certificate, no CN or O fields");
// Everything is peachy. Return some useful data to the caller.
PkiVerificationData data = new PkiVerificationData(entityName, orgName, publicKey, 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;
}
try {
// Check if we are on Android.
Class version = Class.forName("android.os.Build$VERSION");
// Build.VERSION_CODES.ICE_CREAM_SANDWICH is 14.
if (version.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. Fall through.
} catch (NoSuchFieldException e) {
throw new RuntimeException(e); // Should never happen.
} catch (IllegalAccessException e) {
throw new RuntimeException(e); // Should never happen.
}
if (path == null) {
path = System.getProperty("javax.net.ssl.trustStore");
}
if (path == null) {
// 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. This can fail on Android
// but we should never reach it there.
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.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(NetworkParameters.MAX_MONEY) > 0)
throw new PaymentRequestException.InvalidOutputs("The outputs are way too big.");
} catch (InvalidProtocolBufferException e) {
throw new PaymentRequestException(e);
}
}
/** 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;
}
}