xmrwallet/app/src/main/java/com/m2049r/xmrwallet/data/BarcodeData.java

327 lines
11 KiB
Java

/*
* Copyright (c) 2017 m2049r
*
* 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.m2049r.xmrwallet.data;
import android.net.Uri;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.util.BitcoinAddressValidator;
import com.m2049r.xmrwallet.util.OpenAliasHelper;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
import timber.log.Timber;
public class BarcodeData {
public static final String XMR_SCHEME = "monero:";
public static final String XMR_PAYMENTID = "tx_payment_id";
public static final String XMR_AMOUNT = "tx_amount";
public static final String XMR_DESCRIPTION = "tx_description";
public static final String OA_XMR_ASSET = "xmr";
public static final String OA_BTC_ASSET = "btc";
static final String BTC_SCHEME = "bitcoin";
static final String BTC_DESCRIPTION = "message";
static final String BTC_AMOUNT = "amount";
public enum Asset {
XMR, BTC
}
public enum Security {
NORMAL,
OA_NO_DNSSEC,
OA_DNSSEC
}
final public Asset asset;
final public String address;
final public String addressName;
final public String amount;
final public String description;
final public Security security;
public BarcodeData(Asset asset, String address) {
this(asset, address, null, null, null, Security.NORMAL);
}
public BarcodeData(Asset asset, String address, String amount) {
this(asset, address, null, null, amount, Security.NORMAL);
}
public BarcodeData(Asset asset, String address, String amount, String description, Security security) {
this(asset, address, null, description, amount, security);
}
public BarcodeData(Asset asset, String address, String paymentId, String description, String amount) {
this(asset, address, null, description, amount, Security.NORMAL);
}
public BarcodeData(Asset asset, String address, String description, String amount) {
this(asset, address, null, description, amount, Security.NORMAL);
}
public BarcodeData(Asset asset, String address, String addressName, String description, String amount, Security security) {
this.asset = asset;
this.address = address;
this.addressName = addressName;
this.description = description;
this.amount = amount;
this.security = security;
}
public Uri getUri() {
return Uri.parse(getUriString());
}
public String getUriString() {
if (asset != Asset.XMR) throw new IllegalStateException("We can only do XMR stuff!");
StringBuilder sb = new StringBuilder();
sb.append(BarcodeData.XMR_SCHEME).append(address);
boolean first = true;
if ((description != null) && !description.isEmpty()) {
sb.append(first ? "?" : "&");
first = false;
sb.append(BarcodeData.XMR_DESCRIPTION).append('=').append(Uri.encode(description));
}
if ((amount != null) && !amount.isEmpty()) {
sb.append(first ? "?" : "&");
sb.append(BarcodeData.XMR_AMOUNT).append('=').append(amount);
}
return sb.toString();
}
static public BarcodeData fromQrCode(String qrCode) {
// check for monero uri
BarcodeData bcData = parseMoneroUri(qrCode);
// check for naked monero address / integrated address
if (bcData == null) {
bcData = parseMoneroNaked(qrCode);
}
// check for btc uri
if (bcData == null) {
bcData = parseBitcoinUri(qrCode);
}
// check for naked btc address
if (bcData == null) {
bcData = parseBitcoinNaked(qrCode);
}
// check for OpenAlias
if (bcData == null) {
bcData = parseOpenAlias(qrCode, false);
}
return bcData;
}
/**
* Parse and decode a monero scheme string. It is here because it needs to validate the data.
*
* @param uri String containing a monero URL
* @return BarcodeData object or null if uri not valid
*/
static public BarcodeData parseMoneroUri(String uri) {
Timber.d("parseMoneroUri=%s", uri);
if (uri == null) return null;
if (!uri.startsWith(XMR_SCHEME)) return null;
String noScheme = uri.substring(XMR_SCHEME.length());
Uri monero = Uri.parse(noScheme);
Map<String, String> parms = new HashMap<>();
String query = monero.getEncodedQuery();
if (query != null) {
String[] args = query.split("&");
for (String arg : args) {
String[] namevalue = arg.split("=");
if (namevalue.length == 0) {
continue;
}
parms.put(Uri.decode(namevalue[0]).toLowerCase(),
namevalue.length > 1 ? Uri.decode(namevalue[1]) : "");
}
}
String address = monero.getPath();
String paymentId = parms.get(XMR_PAYMENTID);
// no support for payment ids!
if (paymentId != null) {
Timber.e("no support for payment ids!");
return null;
}
String description = parms.get(XMR_DESCRIPTION);
String amount = parms.get(XMR_AMOUNT);
if (amount != null) {
try {
Double.parseDouble(amount);
} catch (NumberFormatException ex) {
Timber.d(ex.getLocalizedMessage());
return null; // we have an amount but its not a number!
}
}
if ((address == null) || !Wallet.isAddressValid(address)) {
Timber.d("address invalid");
return null;
}
return new BarcodeData(Asset.XMR, address, description, amount);
}
static public BarcodeData parseMoneroNaked(String address) {
Timber.d("parseMoneroNaked=%s", address);
if (address == null) return null;
if (!Wallet.isAddressValid(address)) {
Timber.d("address invalid");
return null;
}
return new BarcodeData(Asset.XMR, address);
}
// bitcoin:mpQ84J43EURZHkCnXbyQ4PpNDLLBqdsMW2?amount=0.01
// bitcoin:?r=https://bitpay.com/i/xxx
static public BarcodeData parseBitcoinUri(String uriString) {
Timber.d("parseBitcoinUri=%s", uriString);
if (uriString == null) return null;
URI uri;
try {
uri = new URI(uriString);
} catch (URISyntaxException ex) {
return null;
}
if (!uri.isOpaque() ||
!uri.getScheme().equals(BTC_SCHEME)) return null;
String[] parts = uri.getRawSchemeSpecificPart().split("[?]");
if ((parts.length <= 0) || (parts.length > 2)) {
Timber.d("invalid number of parts %d", parts.length);
return null;
}
Map<String, String> parms = new HashMap<>();
if (parts.length == 2) {
String[] args = parts[1].split("&");
for (String arg : args) {
String[] namevalue = arg.split("=");
if (namevalue.length == 0) {
continue;
}
parms.put(Uri.decode(namevalue[0]).toLowerCase(),
namevalue.length > 1 ? Uri.decode(namevalue[1]) : "");
}
}
String description = parms.get(BTC_DESCRIPTION);
String address = parts[0]; // no need to decode as there can bo no special characters
if (address.isEmpty()) {
Timber.d("no address");
return null;
}
if (!BitcoinAddressValidator.validate(address)) {
Timber.d("BTC address (%s) invalid", address);
return null;
}
String amount = parms.get(BTC_AMOUNT);
if ((amount != null) && (!amount.isEmpty())) {
try {
Double.parseDouble(amount);
} catch (NumberFormatException ex) {
Timber.d(ex.getLocalizedMessage());
return null; // we have an amount but its not a number!
}
}
return new BarcodeData(BarcodeData.Asset.BTC, address, description, amount);
}
static public BarcodeData parseBitcoinNaked(String address) {
Timber.d("parseBitcoinNaked=%s", address);
if (address == null) return null;
if (!BitcoinAddressValidator.validate(address)) {
Timber.d("address invalid");
return null;
}
return new BarcodeData(BarcodeData.Asset.BTC, address);
}
static public BarcodeData parseOpenAlias(String oaString, boolean dnssec) {
Timber.d("parseOpenAlias=%s", oaString);
if (oaString == null) return null;
Map<String, String> oaAttrs = OpenAliasHelper.parse(oaString);
if (oaAttrs == null) return null;
String oaAsset = oaAttrs.get(OpenAliasHelper.OA1_ASSET);
if (oaAsset == null) return null;
String address = oaAttrs.get(OpenAliasHelper.OA1_ADDRESS);
if (address == null) return null;
Asset asset;
if (OA_XMR_ASSET.equals(oaAsset)) {
if (!Wallet.isAddressValid(address)) {
Timber.d("XMR address invalid");
return null;
}
asset = Asset.XMR;
} else if (OA_BTC_ASSET.equals(oaAsset)) {
if (!BitcoinAddressValidator.validate(address)) {
Timber.d("BTC address invalid");
return null;
}
asset = Asset.BTC;
} else {
Timber.i("Unsupported OpenAlias asset %s", oaAsset);
return null;
}
String paymentId = oaAttrs.get(OpenAliasHelper.OA1_PAYMENTID);
if (paymentId != null) {
Timber.e("paymentId not supported");
return null;
}
String description = oaAttrs.get(OpenAliasHelper.OA1_DESCRIPTION);
if (description == null) {
description = oaAttrs.get(OpenAliasHelper.OA1_NAME);
}
String amount = oaAttrs.get(OpenAliasHelper.OA1_AMOUNT);
String addressName = oaAttrs.get(OpenAliasHelper.OA1_NAME);
if (amount != null) {
try {
Double.parseDouble(amount);
} catch (NumberFormatException ex) {
Timber.d(ex.getLocalizedMessage());
return null; // we have an amount but its not a number!
}
}
Security sec = dnssec ? BarcodeData.Security.OA_DNSSEC : BarcodeData.Security.OA_NO_DNSSEC;
return new BarcodeData(asset, address, addressName, description, amount, sec);
}
}