copay/src/providers/incoming-data/incoming-data.ts

341 lines
11 KiB
TypeScript

import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { App, Events, NavController } from 'ionic-angular';
import { Logger } from '../../providers/logger/logger';
// providers
import { AppProvider } from '../app/app';
import { BwcProvider } from '../bwc/bwc';
import { PayproProvider } from '../paypro/paypro';
import { PopupProvider } from '../popup/popup';
import { ScanProvider } from '../scan/scan';
// pages
import { ImportWalletPage } from '../../pages/add/import-wallet/import-wallet';
import { JoinWalletPage } from '../../pages/add/join-wallet/join-wallet';
import { BitPayCardIntroPage } from '../../pages/integrations/bitpay-card/bitpay-card-intro/bitpay-card-intro';
import { CoinbasePage } from '../../pages/integrations/coinbase/coinbase';
import { GlideraPage } from '../../pages/integrations/glidera/glidera';
import { AmountPage } from '../../pages/send/amount/amount';
import { ConfirmPage } from '../../pages/send/confirm/confirm';
@Injectable()
export class IncomingDataProvider {
private navCtrl: NavController;
constructor(
private app: App,
private events: Events,
private bwcProvider: BwcProvider,
private payproProvider: PayproProvider,
private scanProvider: ScanProvider,
private popupProvider: PopupProvider,
private logger: Logger,
private appProvider: AppProvider,
private translate: TranslateService
) {
this.logger.info('IncomingDataProvider initialized.');
}
public showMenu(data: any): void {
this.events.publish('incomingDataMenu.showMenu', data);
}
public redir(data: string): boolean {
// TODO Injecting NavController in constructor of service fails with no provider error
this.navCtrl = this.app.getActiveNav();
// data extensions for Payment Protocol with non-backwards-compatible request
if ((/^bitcoin(cash)?:\?r=[\w+]/).exec(data)) {
let coin = 'btc';
if (data.indexOf('bitcoincash') === 0) coin = 'bch';
data = decodeURIComponent(data.replace(/bitcoin(cash)?:\?r=/, ''));
this.payproProvider.getPayProDetails(data, coin).then((details) => {
this.handlePayPro(details, coin);
}).catch((err) => {
this.popupProvider.ionicAlert(this.translate.instant('Error'), err);
});
return true;
}
data = this.sanitizeUri(data);
let amount: string;
let message: string;
let addr: string;
let parsed: any;
let coin: string;
// Bitcoin URL
if (this.bwcProvider.getBitcore().URI.isValid(data)) {
this.logger.debug('Handling Bitcoin URI');
coin = 'btc';
parsed = this.bwcProvider.getBitcore().URI(data);
addr = parsed.address ? parsed.address.toString() : '';
message = parsed.message;
amount = parsed.amount ? parsed.amount : '';
if (parsed.r) {
this.payproProvider.getPayProDetails(parsed.r, coin).then((details) => {
this.handlePayPro(details, coin);
}).catch((err: string) => {
if (addr && amount) this.goSend(addr, amount, message, coin);
else this.popupProvider.ionicAlert(this.translate.instant('Error'), err);
});
} else {
this.goSend(addr, amount, message, coin);
}
return true;
// Cash URI
} else if (this.bwcProvider.getBitcoreCash().URI.isValid(data)) {
this.logger.debug('Handling Bitcoin Cash URI');
coin = 'bch';
parsed = this.bwcProvider.getBitcoreCash().URI(data);
addr = parsed.address ? parsed.address.toString() : '';
// keep address in original format
if (parsed.address && data.indexOf(addr) < 0) {
addr = parsed.address.toCashAddress();
};
message = parsed.message;
amount = parsed.amount ? parsed.amount : '';
// paypro not yet supported on cash
if (parsed.r) {
this.payproProvider.getPayProDetails(parsed.r, coin).then((details: any) => {
this.handlePayPro(details, coin);
}).catch((err: string) => {
if (addr && amount)
this.goSend(addr, amount, message, coin);
else
this.popupProvider.ionicAlert(this.translate.instant('Error'), err);
});
} else {
this.goSend(addr, amount, message, coin);
}
return true;
// Cash URI with bitcoin core address version number?
} else if (this.bwcProvider.getBitcore().URI.isValid(data.replace(/^bitcoincash:/, 'bitcoin:'))) {
this.logger.debug('Handling bitcoincash URI with legacy address');
coin = 'bch';
parsed = this.bwcProvider.getBitcore().URI(data.replace(/^bitcoincash:/, 'bitcoin:'));
let oldAddr = parsed.address ? parsed.address.toString() : '';
if (!oldAddr) return false;
addr = '';
let a = this.bwcProvider.getBitcore().Address(oldAddr).toObject();
addr = this.bwcProvider.getBitcoreCash().Address.fromObject(a).toString();
// Translate address
this.logger.debug('address transalated to:' + addr);
let title = this.translate.instant('Bitcoin cash Payment');
let msg = this.translate.instant('Payment address was translated to new Bitcoin Cash address format: {{addr}}', { addr });
this.popupProvider.ionicConfirm(title, msg).then((res: boolean) => {
if (!res) return false;
message = parsed.message;
amount = parsed.amount ? parsed.amount : '';
// paypro not yet supported on cash
if (parsed.r) {
this.payproProvider.getPayProDetails(parsed.r, coin).then((details) => {
this.handlePayPro(details, coin);
}).catch((err) => {
if (addr && amount)
this.goSend(addr, amount, message, coin);
else
this.popupProvider.ionicAlert(this.translate.instant('Error'), err);
});
} else {
this.goSend(addr, amount, message, coin);
}
});
return true;
} else if (/^https?:\/\//.test(data)) {
// Plain URL
this.logger.debug('Handling Plain URL');
let coin = 'btc'; // Assume BTC
this.payproProvider.getPayProDetails(data, coin).then((details) => {
this.handlePayPro(details, coin);
return true;
}).catch(() => {
this.showMenu({
data,
type: 'url'
});
return;
});
// Plain Address
} else if (this.bwcProvider.getBitcore().Address.isValid(data, 'livenet') || this.bwcProvider.getBitcore().Address.isValid(data, 'testnet')) {
this.logger.debug('Handling Bitcoin Plain Address');
if (this.navCtrl.getActive().name === 'ScanPage') {
this.showMenu({
data,
type: 'bitcoinAddress',
coin: 'btc'
});
} else {
let coin = 'btc';
this.goToAmountPage(data, coin);
}
} else if (this.bwcProvider.getBitcoreCash().Address.isValid(data, 'livenet')) {
this.logger.debug('Handling Bitcoin Cash Plain Address');
if (this.navCtrl.getActive().name === 'ScanPage') {
this.showMenu({
data,
type: 'bitcoinAddress',
coin: 'bch',
});
} else {
let coin = 'bch';
this.goToAmountPage(data, coin);
}
} else if (data && data.indexOf(this.appProvider.info.name + '://glidera') === 0) {
let code = this.getParameterByName('code', data);
this.navCtrl.push(GlideraPage, { code });
this.logger.debug('Glidera TODO');
return true;
} else if (data && data.indexOf(this.appProvider.info.name + '://coinbase') === 0) {
let code = this.getParameterByName('code', data);
this.navCtrl.push(CoinbasePage, { code });
this.logger.debug('Coinbase TODO');
return true;
// BitPayCard Authentication
} else if (data && data.indexOf(this.appProvider.info.name + '://') === 0) {
// Disable BitPay Card
if (!this.appProvider.info._enabledExtensions.debitcard) return false;
// For BitPay card binding
let secret = this.getParameterByName('secret', data);
let email = this.getParameterByName('email', data);
let otp = this.getParameterByName('otp', data);
let reason = this.getParameterByName('r', data);
switch (reason) {
default:
case '0':
/* For BitPay card binding */
this.navCtrl.parent.select(0);
this.navCtrl.push(BitPayCardIntroPage, { secret, email, otp });
break;
}
return true;
// Join
} else if (data && data.match(/^copay:[0-9A-HJ-NP-Za-km-z]{70,80}$/)) {
this.navCtrl.push(JoinWalletPage, { url: data, fromScan: true })
return true;
// Old join
} else if (data && data.match(/^[0-9A-HJ-NP-Za-km-z]{70,80}$/)) {
this.navCtrl.push(JoinWalletPage, { url: data, fromScan: true })
return true;
} else if (data && (data.substring(0, 2) == '6P' || this.checkPrivateKey(data))) {
this.logger.debug('Handling private key');
this.showMenu({
data,
type: 'privateKey'
});
} else if (data && ((data.substring(0, 2) == '1|') || (data.substring(0, 2) == '2|') || (data.substring(0, 2) == '3|'))) {
this.navCtrl.push(ImportWalletPage, { code: data, fromScan: true })
return true;
} else {
if (this.navCtrl.getActive().name === 'ScanPage') {
this.logger.debug('Handling plain text');
this.showMenu({
data,
type: 'text'
});
}
}
return false;
}
private sanitizeUri(data: any): string {
// Fixes when a region uses comma to separate decimals
let regex = /[\?\&]amount=(\d+([\,\.]\d+)?)/i;
let match = regex.exec(data);
if (!match || match.length === 0) {
return data;
}
let value = match[0].replace(',', '.');
let newUri = data.replace(regex, value);
// mobile devices, uris like copay://glidera
newUri.replace('://', ':');
return newUri;
}
private getParameterByName(name: string, url: string): string {
if (!url) return;
name = name.replace(/[\[\]]/g, "\\$&");
let regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
results = regex.exec(url);
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, " "));
}
private checkPrivateKey(privateKey: string): boolean {
try {
this.bwcProvider.getBitcore().PrivateKey(privateKey, 'livenet');
} catch (err) {
return false;
}
return true;
}
private goSend(addr: string, amount: string, message: string, coin: string): void {
if (amount) {
this.navCtrl.push(ConfirmPage, {
amount,
toAddress: addr,
description: message,
coin
});
} else {
this.navCtrl.push(AmountPage, {
toAddress: addr,
coin
});
}
}
private goToAmountPage(toAddress: string, coin: string) {
this.navCtrl.push(AmountPage, {
toAddress,
coin
});
}
private handlePayPro(payProDetails: any, coin?: string): void {
let stateParams: any = {
amount: payProDetails.amount,
toAddress: payProDetails.toAddress,
description: payProDetails.memo,
paypro: payProDetails,
coin
};
// fee
if (payProDetails.requiredFeeRate) {
stateParams.requiredFeeRate = payProDetails.requiredFeeRate * 1024;
}
this.scanProvider.pausePreview();
this.navCtrl.push(ConfirmPage, stateParams);
}
}