Skip to content

Commit

Permalink
Handle payment request hash in BIP72 URis.
Browse files Browse the repository at this point in the history
  • Loading branch information
schildbach committed Sep 10, 2014
1 parent b0cfbfd commit 64f8ff4
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 30 deletions.
68 changes: 62 additions & 6 deletions wallet/src/de/schildbach/wallet/data/PaymentIntent.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import android.os.Parcel;
import android.os.Parcelable;

Expand All @@ -37,6 +40,7 @@
import com.google.bitcoin.script.Script;
import com.google.bitcoin.script.ScriptBuilder;
import com.google.bitcoin.uri.BitcoinURI;
import com.google.common.io.BaseEncoding;

import de.schildbach.wallet.Constants;
import de.schildbach.wallet.util.Bluetooth;
Expand Down Expand Up @@ -160,9 +164,14 @@ private Output(final Parcel in)
@CheckForNull
public final String paymentRequestUrl;

@CheckForNull
public final byte[] paymentRequestHash;

private static final Logger log = LoggerFactory.getLogger(PaymentIntent.class);

public PaymentIntent(@Nullable final Standard standard, @Nullable final String payeeName, @Nullable final String payeeOrganization,
@Nullable final String payeeVerifiedBy, @Nullable final Output[] outputs, @Nullable final String memo, @Nullable final String paymentUrl,
@Nullable final byte[] payeeData, @Nullable final String paymentRequestUrl)
@Nullable final byte[] payeeData, @Nullable final String paymentRequestUrl, @Nullable final byte[] paymentRequestHash)
{
this.standard = standard;
this.payeeName = payeeName;
Expand All @@ -173,16 +182,17 @@ public PaymentIntent(@Nullable final Standard standard, @Nullable final String p
this.paymentUrl = paymentUrl;
this.payeeData = payeeData;
this.paymentRequestUrl = paymentRequestUrl;
this.paymentRequestHash = paymentRequestHash;
}

private PaymentIntent(@Nonnull final Address address, @Nullable final String addressLabel)
{
this(null, null, null, null, buildSimplePayTo(BigInteger.ZERO, address), addressLabel, null, null, null);
this(null, null, null, null, buildSimplePayTo(BigInteger.ZERO, address), addressLabel, null, null, null, null);
}

public static PaymentIntent blank()
{
return new PaymentIntent(null, null, null, null, null, null, null, null, null);
return new PaymentIntent(null, null, null, null, null, null, null, null, null, null);
}

public static PaymentIntent fromAddress(@Nonnull final Address address, @Nullable final String addressLabel)
Expand All @@ -198,11 +208,29 @@ public static PaymentIntent fromAddress(@Nonnull final String address, @Nullable

public static PaymentIntent fromBitcoinUri(@Nonnull final BitcoinURI bitcoinUri)
{
final Output[] outputs = buildSimplePayTo(bitcoinUri.getAmount(), bitcoinUri.getAddress());
final Address address = bitcoinUri.getAddress();
final Output[] outputs = address != null ? buildSimplePayTo(bitcoinUri.getAmount(), address) : null;
final String bluetoothMac = (String) bitcoinUri.getParameterByName(Bluetooth.MAC_URI_PARAM);
final String paymentRequestHashStr = (String) bitcoinUri.getParameterByName("h");
final byte[] paymentRequestHash = paymentRequestHashStr != null ? base64UrlDecode(paymentRequestHashStr) : null;

return new PaymentIntent(PaymentIntent.Standard.BIP21, null, null, null, outputs, bitcoinUri.getLabel(), bluetoothMac != null ? "bt:"
+ bluetoothMac : null, null, bitcoinUri.getPaymentRequestUrl());
+ bluetoothMac : null, null, bitcoinUri.getPaymentRequestUrl(), paymentRequestHash);
}

private static final BaseEncoding BASE64URL = BaseEncoding.base64Url().omitPadding();

private static byte[] base64UrlDecode(final String encoded)
{
try
{
return BASE64URL.decode(encoded);
}
catch (final IllegalArgumentException x)
{
log.info("cannot base64url-decode: " + encoded);
return null;
}
}

public PaymentIntent mergeWithEditedValues(@Nullable final BigInteger editedAmount, @Nullable final Address editedAddress)
Expand Down Expand Up @@ -233,7 +261,7 @@ public PaymentIntent mergeWithEditedValues(@Nullable final BigInteger editedAmou
outputs = buildSimplePayTo(editedAmount, editedAddress);
}

return new PaymentIntent(standard, payeeName, payeeOrganization, payeeVerifiedBy, outputs, memo, null, payeeData, null);
return new PaymentIntent(standard, payeeName, payeeOrganization, payeeVerifiedBy, outputs, memo, null, payeeData, null, null);
}

public SendRequest toSendRequest()
Expand Down Expand Up @@ -363,12 +391,19 @@ public boolean isBluetoothPaymentRequestUrl()
* Check if given payment intent is only extending on <i>this</i> one, that is it does not alter any of the fields.
* Address and amount fields must be equal, respectively (non-existence included).
*
* Alternatively, a BIP21+BIP72 request can provide a hash of the BIP70 request.
*
* @param other
* payment intent that is checked if it extends this one
* @return true if it extends
*/
public boolean isExtendedBy(final PaymentIntent other)
{
// shortcut via hash
if (standard == Standard.BIP21 && other.standard == Standard.BIP70)
if (paymentRequestHash != null && Arrays.equals(paymentRequestHash, other.paymentRequestHash))
return true;

// TODO memo
return equalsAmount(other) && equalsAddress(other);
}
Expand Down Expand Up @@ -469,6 +504,16 @@ public void writeToParcel(final Parcel dest, final int flags)
}

dest.writeString(paymentRequestUrl);

if (paymentRequestHash != null)
{
dest.writeInt(paymentRequestHash.length);
dest.writeByteArray(paymentRequestHash);
}
else
{
dest.writeInt(0);
}
}

public static final Parcelable.Creator<PaymentIntent> CREATOR = new Parcelable.Creator<PaymentIntent>()
Expand Down Expand Up @@ -521,5 +566,16 @@ private PaymentIntent(final Parcel in)
}

paymentRequestUrl = in.readString();

final int paymentRequestHashLength = in.readInt();
if (paymentRequestHashLength > 0)
{
paymentRequestHash = new byte[paymentRequestHashLength];
in.readByteArray(paymentRequestHash);
}
else
{
paymentRequestHash = null;
}
}
}
10 changes: 3 additions & 7 deletions wallet/src/de/schildbach/wallet/ui/InputParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -103,15 +103,11 @@ else if (input.startsWith("bitcoin:"))
try
{
final BitcoinURI bitcoinUri = new BitcoinURI(null, input);

final Address address = bitcoinUri.getAddress();
if (address == null)
throw new BitcoinURIParseException("missing address");
if (address != null && !Constants.NETWORK_PARAMETERS.equals(address.getParameters()))
throw new BitcoinURIParseException("mismatched network");

if (Constants.NETWORK_PARAMETERS.equals(address.getParameters()))
handlePaymentIntent(PaymentIntent.fromBitcoinUri(bitcoinUri));
else
error(R.string.input_parser_invalid_address, input);
handlePaymentIntent(PaymentIntent.fromBitcoinUri(bitcoinUri));
}
catch (final BitcoinURIParseException x)
{
Expand Down
46 changes: 30 additions & 16 deletions wallet/src/de/schildbach/wallet/ui/send/SendCoinsFragment.java
Original file line number Diff line number Diff line change
Expand Up @@ -550,10 +550,7 @@ public void onClick(final View v)
@Override
public void onClick(final View v)
{
if (state == State.INPUT)
activity.setResult(Activity.RESULT_CANCELED);

activity.finish();
handleCancel();
}
});

Expand Down Expand Up @@ -691,7 +688,7 @@ protected void error(final int messageResId, final Object... messageArgs)
else if (requestCode == REQUEST_CODE_ENABLE_BLUETOOTH_FOR_PAYMENT_REQUEST)
{
if (paymentIntent.isBluetoothPaymentRequestUrl())
requestPaymentRequest(paymentIntent.paymentRequestUrl);
requestPaymentRequest();
}
else if (requestCode == REQUEST_CODE_ENABLE_BLUETOOTH_FOR_DIRECT_PAYMENT)
{
Expand Down Expand Up @@ -775,6 +772,14 @@ else if (addressParams == null)
updateView();
}

private void handleCancel()
{
if (state == State.INPUT)
activity.setResult(Activity.RESULT_CANCELED);

activity.finish();
}

private boolean isOutputsValid()
{
if (paymentIntent.hasOutputs())
Expand Down Expand Up @@ -1329,28 +1334,28 @@ else if (paymentIntent.isHttpPaymentUrl())
if (paymentIntent.isBluetoothPaymentRequestUrl() && !Constants.BUG_OPENSSL_HEARTBLEED)
{
if (bluetoothAdapter.isEnabled())
requestPaymentRequest(paymentIntent.paymentRequestUrl);
requestPaymentRequest();
else
// ask for permission to enable bluetooth
startActivityForResult(new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE),
REQUEST_CODE_ENABLE_BLUETOOTH_FOR_PAYMENT_REQUEST);
}
else if (paymentIntent.isHttpPaymentRequestUrl())
{
requestPaymentRequest(paymentIntent.paymentRequestUrl);
requestPaymentRequest();
}
}
}
});
}

private void requestPaymentRequest(final String paymentRequestUrl)
private void requestPaymentRequest()
{
final String host;
if (!Bluetooth.isBluetoothUrl(paymentRequestUrl))
host = Uri.parse(paymentRequestUrl).getHost();
if (!Bluetooth.isBluetoothUrl(paymentIntent.paymentRequestUrl))
host = Uri.parse(paymentIntent.paymentRequestUrl).getHost();
else
host = Bluetooth.decompressMac(Bluetooth.getBluetoothMac(paymentRequestUrl));
host = Bluetooth.decompressMac(Bluetooth.getBluetoothMac(paymentIntent.paymentRequestUrl));

ProgressDialogFragment.showProgress(fragmentManager, getString(R.string.send_coins_fragment_request_payment_request_progress, host));

Expand All @@ -1363,6 +1368,7 @@ public void onPaymentIntent(final PaymentIntent paymentIntent)

if (SendCoinsFragment.this.paymentIntent.isExtendedBy(paymentIntent))
{
// success
updateStateFrom(paymentIntent);
updateView();
}
Expand Down Expand Up @@ -1397,19 +1403,27 @@ public void onFail(final int messageResId, final Object... messageArgs)
@Override
public void onClick(final DialogInterface dialog, final int which)
{
requestPaymentRequest(paymentRequestUrl);
requestPaymentRequest();
}
});
dialog.setNegativeButton(R.string.button_dismiss, new DialogInterface.OnClickListener()
{
@Override
public void onClick(final DialogInterface dialog, final int which)
{
if (!paymentIntent.hasOutputs())
handleCancel();
}
});
dialog.setNegativeButton(R.string.button_dismiss, null);
dialog.show();
}
};

if (!Bluetooth.isBluetoothUrl(paymentRequestUrl))
if (!Bluetooth.isBluetoothUrl(paymentIntent.paymentRequestUrl))
new RequestPaymentRequestTask.HttpRequestTask(backgroundHandler, callback, application.httpUserAgent())
.requestPaymentRequest(paymentRequestUrl);
.requestPaymentRequest(paymentIntent.paymentRequestUrl);
else
new RequestPaymentRequestTask.BluetoothRequestTask(backgroundHandler, callback, bluetoothAdapter)
.requestPaymentRequest(paymentRequestUrl);
.requestPaymentRequest(paymentIntent.paymentRequestUrl);
}
}
5 changes: 4 additions & 1 deletion wallet/src/de/schildbach/wallet/util/PaymentProtocol.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import com.google.bitcoin.protocols.payments.PaymentSession.PkiVerificationData;
import com.google.bitcoin.script.Script;
import com.google.bitcoin.script.ScriptBuilder;
import com.google.common.hash.Hashing;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.UninitializedMessageException;
Expand Down Expand Up @@ -125,8 +126,10 @@ public static PaymentIntent parsePaymentRequest(@Nonnull final byte[] serialized
final String paymentUrl = paymentDetails.hasPaymentUrl() ? paymentDetails.getPaymentUrl() : null;
final byte[] merchantData = paymentDetails.hasMerchantData() ? paymentDetails.getMerchantData().toByteArray() : null;

final byte[] paymentRequestHash = Hashing.sha256().hashBytes(serializedPaymentRequest).asBytes();

final PaymentIntent paymentIntent = new PaymentIntent(PaymentIntent.Standard.BIP70, pkiName, pkiOrgName, pkiCaName,
outputs.toArray(new PaymentIntent.Output[0]), memo, paymentUrl, merchantData, null);
outputs.toArray(new PaymentIntent.Output[0]), memo, paymentUrl, merchantData, null, paymentRequestHash);

if (paymentIntent.hasPaymentUrl() && !paymentIntent.isSupportedPaymentUrl())
throw new PaymentRequestException.InvalidPaymentURL("cannot handle payment url: " + paymentIntent.paymentUrl);
Expand Down

0 comments on commit 64f8ff4

Please sign in to comment.