This commit is contained in:
Jean-Baptiste Dominguez 2018-09-07 01:41:13 +09:00
commit 230b6e2228
83 changed files with 5830 additions and 2736 deletions

View file

@ -0,0 +1,416 @@
'use strict';
// https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki
// https://github.com/bitcoin/bips/blob/master/bip-0072.mediawiki
(function(){
angular
.module('bitcoincom.services')
.factory('bitcoinUriService', bitcoinUriService);
function bitcoinUriService(bitcoinCashJsService, bwcService, $log) {
var bch = bitcoinCashJsService.getBitcoinCashJs();
var bitcore = bwcService.getBitcore();
var service = {
parse: parse
};
return service;
function bitpayAddrOnMainnet(address) {
var Address = bch.Address;
var BitpayFormat = Address.BitpayFormat;
var mainnet = bch.Networks.mainnet;
var result = null;
if (address[0] == 'C') {
try {
result = Address.fromString(address, mainnet, 'pubkeyhash', BitpayFormat);
} catch (e) {};
} else if (address[0] == 'H') {
try {
result = Address.fromString(address, mainnet, 'scripthash', BitpayFormat);
} catch (e) {};
}
return result;
}
function cashAddrOnMainnet(address) {
var Address = bch.Address;
var CashAddrFormat = Address.CashAddrFormat;
var mainnet = bch.Networks.mainnet;
var prefixed = 'bitcoincash:' + address;
var result = null;
if (address[0] == 'q') {
try {
result = Address.fromString(prefixed, mainnet, 'pubkeyhash', CashAddrFormat);
} catch (e) {};
} else if (address[0] == 'p') {
try {
result = Address.fromString(prefixed, mainnet, 'scripthash', CashAddrFormat);
} catch (e) {};
}
return result;
}
function cashAddrOnTestnet(address) {
var Address = bch.Address;
var CashAddrFormat = Address.CashAddrFormat;
var testnet = bch.Networks.testnet;
var prefixed = 'bchtest:' + address;
var result = null;
if (address[0] == 'q') {
try {
result = Address.fromString(prefixed, testnet, 'pubkeyhash', CashAddrFormat);
} catch (e) {};
} else if (address[0] == 'p') {
try {
result = Address.fromString(prefixed, testnet, 'scripthash', CashAddrFormat);
} catch (e) {};
}
return result;
}
function infoFromWalletImportText(data) {
var split = data.split('|');
// Copay seems to use extra parameter for coin.
if (split.length < 5 || split.length > 6) {
return null;
}
var type = parseInt(split[0], 10);
if (isNaN(type)) {
return null;
}
var data = split[1];
var network = split[2];
if (!(network === 'livenet' || network === 'testnet')) {
return null;
}
var isTestnet = network === 'testnet';
var derivationPath = split[3];
if (!/^m\/\d+'\/\d+'\/\d+'$/.test(derivationPath)) {
return null;
}
var hasPassphraseText = split[4];
if (!(hasPassphraseText === 'true' || hasPassphraseText === 'false')) {
return null;
}
var hasPassphrase = hasPassphraseText === 'true';
var coin; // Intentionally undefined as may not be present
if (split.length > 5) {
var coinText = split[5];
if (!(coinText === 'bch' || coinText === 'btc')) {
return null;
}
coin = coinText;
}
return {
type: type,
data: data,
isTestnet: isTestnet,
derivationPath: derivationPath,
hasPassphrase: hasPassphrase,
coin: coin
};
}
/*
For parsing:
BIP21
BIP72
returns:
{
amount: '',
amountInSatoshis: 0,
bareUrl: '',
coin: '',
copayInvitation: '',
import: { // testnet info in root, coin info in root if available
data: '',
derivationPath: '',
hasPassphrase: false,
type: 1,
},
isValid: false,
label: '',
message: '',
other: {
somethingIDontUnderstand: 'Its value'
},
privateKey: {
encrypted: '',
wif: ''
}'',
publicAddress: {
bitpay: '',
cashAddr: '',
legacy: '',
},
req: {
"req-param0": '',
"req-param1": ''
},
testnet: false,
url: '' // For BIP70
}
Only fields that are present in the data are defined in the returned object. Both privateKey and publicAddress only have 1 field defined, if they exist at all.
The exception to this is the coin property, which is determined from other data, such as the prefix or address type.
*/
function parse(data) {
var parsed = {
isValid: false
};
if (typeof data !== 'string') {
return parsed;
}
// Identify prefix
var trimmed = data.trim();
var colonSplit = /^([\w-]*):?(.*)$/.exec(trimmed);
if (!colonSplit) {
return parsed;
}
var addressAndParams = '';
var preColonLower = colonSplit[1].toLowerCase();
if (preColonLower === 'bitcoin') {
parsed.coin = 'btc';
addressAndParams = colonSplit[2].trim();
console.log('Is btc');
} else if (/^(?:bitcoincash)|(?:bitcoin-cash)$/.test(preColonLower)) {
parsed.coin = 'bch';
parsed.test = false;
addressAndParams = colonSplit[2].trim();
console.log('Is bch');
} else if (/^(?:bchtest)$/.test(preColonLower)) {
parsed.coin = 'bch';
parsed.isTestnet = true;
addressAndParams = colonSplit[2].trim();
console.log('Is bch');
} else if (colonSplit[2] === '') {
// No colon and no coin specifier.
addressAndParams = colonSplit[1].trim();
console.log('No prefix.');
} else if (/^https?$/.test(colonSplit[1])) { // Plain URL
addressAndParams = trimmed;
} else if (colonSplit[2].indexOf('|') == 0) { // Import
addressAndParams = trimmed
} else {
// Something we don't recognise
return parsed;
}
// Remove erroneous leading slashes
//var leadingSlashes = /^\/*([^\/]+(?:.*))$/.exec(addressAndParams);
var leadingSlashes = /^\/*(.*)$/.exec(addressAndParams);
if (!leadingSlashes) {
return parsed;
}
addressAndParams = leadingSlashes[1];
var questionMarkSplit = /^([^\?]*)\??([^\?]*)$/.exec(addressAndParams);
if (!questionMarkSplit) {
return parsed;
}
var address = questionMarkSplit[1];
var params = questionMarkSplit[2];
if (params.length > 0) {
var paramsSplit = params.split('&');
var others;
var req;
var paramCount = paramsSplit.length;
for(var i = 0; i < paramCount; i++) {
var param = paramsSplit[i];
var valueSplit = param.split('=');
if (valueSplit.length !== 2) {
return parsed;
}
var key = valueSplit[0];
var value = valueSplit[1];
var decodedValue = decodeURIComponent(value);
switch(key) {
case 'amount':
var amount = parseFloat(decodedValue);
if (amount) { // Checking for NaN, or no numbers at all etc. & convert to satoshi
parsed.amount = decodedValue; // Need to check if a currency is precised
parsed.amountInSatoshis = amount * 100000000
} else {
return parsed;
}
break;
case 'label':
parsed.label = decodedValue;
break;
case 'message':
parsed.message = decodedValue;
break;
case 'r':
// Could use a more comprehesive regex to test URL validity, but then how would we know
// which part of the validation it failed?
if (decodedValue.startsWith('https://')) {
parsed.url = decodedValue;
} else {
return parsed;
}
break;
default:
if (key.startsWith('req-')) {
req = req || {};
req[key] = decodedValue;
} else {
others = others || {};
others[key] = decodedValue;
}
}
};
}
parsed.others = others;
parsed.req = req;
if (address) {
var addressLowerCase = address.toLowerCase();
var copayInvitationRe = /^[0-9A-HJ-NP-Za-km-z]{70,80}$/;
//var legacyRe = /^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$/;
//var legacyTestnetRe = /^[mn][a-km-zA-HJ-NP-Z1-9]{25,34}$/;
var privateKeyEncryptedRe = /^6P[1-9A-HJ-NP-Za-km-z]{56}$/;
var privateKeyForUncompressedPublicKeyRe = /^5[1-9A-HJ-NP-Za-km-z]{50}$/;
var privateKeyForUncompressedPublicKeyTestnetRe = /^9[1-9A-HJ-NP-Za-km-z]{50}$/;
var privateKeyForCompressedPublicKeyRe = /^[KL][1-9A-HJ-NP-Za-km-z]{51}$/;
var privateKeyForCompressedPublicKeyTestnetRe = /^[c][1-9A-HJ-NP-Za-km-z]{51}$/;
var urlRe = /^https?:\/\/.+/;
var bitpayAddrMainnet = bitpayAddrOnMainnet(address);
var cashAddrTestnet = cashAddrOnTestnet(addressLowerCase);
var cashAddrMainnet = cashAddrOnMainnet(addressLowerCase);
var importInfo = infoFromWalletImportText(address);
var privateKey = '';
if (parsed.isTestnet && cashAddrTestnet) {
parsed.address = addressLowerCase;
parsed.coin = 'bch';
parsed.publicAddress = {
cashAddr: addressLowerCase
};
parsed.isValid = true;
} else if (cashAddrMainnet) {
parsed.coin = 'bch';
parsed.publicAddress = {
cashAddr: addressLowerCase
};
parsed.isTestnet = false;
parsed.isValid = true;
} else if (bitcore.Address.isValid(address, 'livenet')) {
parsed.publicAddress = {
legacy: address
};
parsed.isTestnet = false;
parsed.isValid = true;
} else if (bitcore.Address.isValid(address, 'testnet')) {
parsed.publicAddress = {
legacy: address
};
parsed.isTestnet = true;
parsed.isValid = true;
} else if (bitpayAddrMainnet) {
parsed.coin = 'bch';
parsed.publicAddress = {
bitpay: address
};
parsed.isTestnet = false;
parsed.isValid = true;
} else if (copayInvitationRe.test(address) ) {
parsed.copayInvitation = address;
parsed.isValid = true;
} else if (privateKeyForUncompressedPublicKeyRe.test(address) || privateKeyForCompressedPublicKeyRe.test(address)) {
privateKey = address;
try {
new bitcore.PrivateKey(privateKey, 'livenet');
parsed.privateKey = { wif: privateKey };
parsed.isTestnet = false;
parsed.isValid = true;
} catch (e) {}
} else if (privateKeyForUncompressedPublicKeyTestnetRe.test(address) || privateKeyForCompressedPublicKeyTestnetRe.test(address)) {
privateKey = address;
try {
new bitcore.PrivateKey(privateKey, 'testnet');
parsed.privateKey = { wif: privateKey };
parsed.isTestnet = true;
parsed.isValid = true;
} catch (e) {}
} else if (privateKeyEncryptedRe.test(address)) {
parsed.privateKey = { encrypted: address };
parsed.isValid = true;
} else if (urlRe.test(address)) {
parsed.bareUrl = trimmed;
parsed.isValid = true;
} else if (importInfo) {
parsed.import = {
type: importInfo.type,
data: importInfo.data,
derivationPath: importInfo.derivationPath,
hasPassphrase: importInfo.hasPassphrase
};
parsed.coin = importInfo.coin;
parsed.isTestnet = importInfo.isTestnet;
parsed.isValid = true;
}
} else {
parsed.isValid = !!parsed.url; // BIP72
}
return parsed;
}
}
})();

View file

@ -0,0 +1,429 @@
describe('bitcoinUriService', function() {
var bitcoinUriService;
beforeEach(function() {
module('bitcoinCashJsModule');
module('bitcoincom.services');
module('bwcModule');
inject(function($injector){
bitcoinUriService = $injector.get('bitcoinUriService');
});
});
it('Bitcoin BIP72', function() {
var parsed = bitcoinUriService.parse('bitcoin:?r=https://bitpay.com/i/CwzbKP3k3JNgXJBfuoerDr');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('btc');
expect(parsed.isTestnet).toBeUndefined();
expect(parsed.publicAddress).toBeUndefined();
expect(parsed.url).toBe('https://bitpay.com/i/CwzbKP3k3JNgXJBfuoerDr');
});
it('Bitcoin Cash BIP72', function() {
var parsed = bitcoinUriService.parse('bitcoincash:?r=https://bitpay.com/i/SmHdie5dvBnG5kouZzEPzu');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('bch');
expect(parsed.publicAddress).toBeUndefined();
expect(parsed.isTestnet).toBeUndefined();
expect(parsed.url).toBe('https://bitpay.com/i/SmHdie5dvBnG5kouZzEPzu');
});
it('Bitcoin Cash prefix with legacy address', function() {
var parsed = bitcoinUriService.parse('bitcoincash:1G9FA9fFnHfTYxvmXeAbBD9FwzPAVMbd3j');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('bch');
expect(parsed.publicAddress.legacy).toBe('1G9FA9fFnHfTYxvmXeAbBD9FwzPAVMbd3j');
expect(parsed.isTestnet).toBe(false);
});
it('Bitcoin Cash prefix with legacy address on testnet', function() {
var parsed = bitcoinUriService.parse('bitcoincash:mkDQrKfSFD441JxrD1iPBsJFExgkvrPGQn');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('bch');
expect(parsed.publicAddress.legacy).toBe('mkDQrKfSFD441JxrD1iPBsJFExgkvrPGQn');
expect(parsed.isTestnet).toBe(true);
});
it('Bitcoin Cash uri with extended params', function() {
var parsed = bitcoinUriService.parse('bitcoincash:qr8v2vqnzntykakht43rqmxq8cdjzjp795fc3vsjgc?unknown=something&mystery=Melton%20probang&req-one=ichi&req-beta=Ni%20san');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('bch');
expect(parsed.others.mystery).toBe('Melton probang');
expect(parsed.others.unknown).toBe('something');
expect(parsed.publicAddress.cashAddr).toBe('qr8v2vqnzntykakht43rqmxq8cdjzjp795fc3vsjgc');
expect(parsed.req['req-beta']).toBe('Ni san');
expect(parsed.req['req-one']).toBe('ichi');
expect(parsed.isTestnet).toBe(false);
});
it('Bitcoin Cash uri with invalid amount', function() {
var parsed = bitcoinUriService.parse('bitcoincash:qq0knhwj4d5zy3kdph24w6etq58vwzua6sm7lhcmuk?amount=three');
expect(parsed.isValid).toBe(false);
});
it('Bitcoin testnet address', function() {
var parsed = bitcoinUriService.parse('mtWcoToWhbtPoCby5fvs8xdBujT5GGenD4');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBeUndefined();
expect(parsed.publicAddress.legacy).toBe('mtWcoToWhbtPoCby5fvs8xdBujT5GGenD4');
expect(parsed.isTestnet).toBe(true);
});
it('Bitcoin uri', function() {
var parsed = bitcoinUriService.parse('bitcoin:15yCdKWVKRvfXMJpPYZBqMhiGKwjKzZdLN');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('btc');
expect(parsed.publicAddress.legacy).toBe('15yCdKWVKRvfXMJpPYZBqMhiGKwjKzZdLN');
expect(parsed.isTestnet).toBe(false);
});
it('Bitcoin uri with encoded label', function() {
var parsed = bitcoinUriService.parse('bitcoin:1MxudKDEBWZ1yjizUSf6htacenNtb3DWbT?label=Mr.%20Smith');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('btc');
expect(parsed.label).toBe('Mr. Smith');
expect(parsed.publicAddress.legacy).toBe('1MxudKDEBWZ1yjizUSf6htacenNtb3DWbT');
expect(parsed.isTestnet).toBe(false);
});
it('Bitcoin uri with params', function() {
var parsed = bitcoinUriService.parse('bitcoin:12nCRhMDfxVnuF3uYMXv2fNxBohNmacfWu?amount=20.3&label=Luke-Jr&message=Donation%20for%20project%20xyz');
expect(parsed.isValid).toBe(true);
expect(parsed.amount).toBe('20.3');
expect(parsed.amountInSatoshis).toBe(2030000000);
expect(parsed.coin).toBe('btc');
expect(parsed.label).toBe('Luke-Jr');
expect(parsed.publicAddress.legacy).toBe('12nCRhMDfxVnuF3uYMXv2fNxBohNmacfWu');
expect(parsed.message).toBe('Donation for project xyz');
expect(parsed.isTestnet).toBe(false);
});
it('Bitcoin uri with slash', function() {
var parsed = bitcoinUriService.parse('bitcoin:/1GhpYmbRaf73AZRxDwAGr6653iZBGzdgeA');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('btc');
expect(parsed.publicAddress.legacy).toBe('1GhpYmbRaf73AZRxDwAGr6653iZBGzdgeA');
expect(parsed.isTestnet).toBe(false);
});
it('Bitcoin uri with slashes', function() {
var parsed = bitcoinUriService.parse('bitcoin://18PCPhgZJjLxe9g3Q1BXLpL5aVut1fW3aX');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('btc');
expect(parsed.publicAddress.legacy).toBe('18PCPhgZJjLxe9g3Q1BXLpL5aVut1fW3aX');
expect(parsed.isTestnet).toBe(false);
});
it('Bitcoin uri with space', function() {
var parsed = bitcoinUriService.parse('bitcoin: 19cPoKU5ZazY8NkLEsxK7drBqJnpGkax3d');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('btc');
expect(parsed.publicAddress.legacy).toBe('19cPoKU5ZazY8NkLEsxK7drBqJnpGkax3d');
expect(parsed.isTestnet).toBe(false);
});
it('Bitpay without prefix', function() {
var parsed = bitcoinUriService.parse('CJoRov8TirekvajiimQpb5Hk95evA7H2Yz');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('bch');
expect(parsed.publicAddress.bitpay).toBe('CJoRov8TirekvajiimQpb5Hk95evA7H2Yz');
expect(parsed.isTestnet).toBe(false);
});
it('legacy address', function() {
var parsed = bitcoinUriService.parse('1JXeGEu7bNEAYu6URT6dU6g1Ys6ffSAWYW');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBeUndefined();
expect(parsed.publicAddress.legacy).toBe('1JXeGEu7bNEAYu6URT6dU6g1Ys6ffSAWYW');
expect(parsed.isTestnet).toBe(false);
});
it('cashAddr testnet with prefix', function() {
var parsed = bitcoinUriService.parse('bchtest:qpcz6pmurq9ctg5848trzz9zmuuygj4q5qam7ph3gt');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('bch');
expect(parsed.publicAddress.cashAddr).toBe('qpcz6pmurq9ctg5848trzz9zmuuygj4q5qam7ph3gt');
expect(parsed.isTestnet).toBe(true);
});
it('cashAddr uppercase', function() {
var parsed = bitcoinUriService.parse('BITCOINCASH:QZZG9NMC5VX8GAP6XFATX3TWNSDN2YRMCSSULSMY44');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('bch');
expect(parsed.publicAddress.cashAddr).toBe('qzzg9nmc5vx8gap6xfatx3twnsdn2yrmcssulsmy44');
expect(parsed.isTestnet).toBe(false);
});
it('cashAddr with dash', function() {
var parsed = bitcoinUriService.parse('bitcoin-cash:qpshfu3dk5s3e7zdcgdcun6xgxtra6uyxs7g580js0');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('bch');
expect(parsed.publicAddress.cashAddr).toBe('qpshfu3dk5s3e7zdcgdcun6xgxtra6uyxs7g580js0');
expect(parsed.isTestnet).toBe(false);
});
it('cashAddr with prefix', function() {
var parsed = bitcoinUriService.parse('bitcoincash:qrq9p82a247lecv08ldk5p5h6ahtnjzpqcnh8yhq92');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('bch');
expect(parsed.publicAddress.cashAddr).toBe('qrq9p82a247lecv08ldk5p5h6ahtnjzpqcnh8yhq92');
expect(parsed.isTestnet).toBe(false);
});
it('cashAddr with slash', function() {
var parsed = bitcoinUriService.parse('bitcoincash:/qzdectfmuw0xxztfx7mh045830dqcshj85hr44l35a');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('bch');
expect(parsed.publicAddress.cashAddr).toBe('qzdectfmuw0xxztfx7mh045830dqcshj85hr44l35a');
expect(parsed.isTestnet).toBe(false);
});
it('cashAddr with slashes', function() {
var parsed = bitcoinUriService.parse('bitcoincash://qpj966w8utue75lqqq3rlgh20zkz3rmydqpq8syv9c');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('bch');
expect(parsed.publicAddress.cashAddr).toBe('qpj966w8utue75lqqq3rlgh20zkz3rmydqpq8syv9c');
expect(parsed.isTestnet).toBe(false);
});
it('cashAddr with space', function() {
var parsed = bitcoinUriService.parse('bitcoincash: qpar9ldle8z6alcwgclejdhc24ha2xrg0szs5802ce');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('bch');
expect(parsed.publicAddress.cashAddr).toBe('qpar9ldle8z6alcwgclejdhc24ha2xrg0szs5802ce');
expect(parsed.isTestnet).toBe(false);
});
it('cashAddr with space on testnet', function() {
var parsed = bitcoinUriService.parse('bchtest: qqjxkmtaxk4nv6w9h5ht2fjcj9c7ruh0fu7cnxsx5j');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('bch');
expect(parsed.publicAddress.cashAddr).toBe('qqjxkmtaxk4nv6w9h5ht2fjcj9c7ruh0fu7cnxsx5j');
expect(parsed.isTestnet).toBe(true);
});
it('cashAddr without prefix', function() {
var parsed = bitcoinUriService.parse('qqen2y3l28dpk0dzsag8w027ds96u7z4pc0uxtl0nq');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('bch');
expect(parsed.publicAddress.cashAddr).toBe('qqen2y3l28dpk0dzsag8w027ds96u7z4pc0uxtl0nq');
expect(parsed.isTestnet).toBe(false);
});
it('copay invitation', function() {
var parsed = bitcoinUriService.parse('PD5B7rEEj72st9d5nFszyuKxJP6FAGS7idVC2SMqiMxUcWVd8JifZDJw1UgjUctxefUFE3Sz6qLbch');
expect(parsed.isValid).toBe(true);
expect(parsed.copayInvitation).toBe('PD5B7rEEj72st9d5nFszyuKxJP6FAGS7idVC2SMqiMxUcWVd8JifZDJw1UgjUctxefUFE3Sz6qLbch');
});
it ('import BCH wallet no password', function() {
var parsed = bitcoinUriService.parse("1|suggest route obvious broccoli good position hidden tone history around final lobster|livenet|m/44'/0'/0'|false");
expect(parsed.isValid).toBe(true);
expect(parsed.import.type).toBe(1);
expect(parsed.import.data).toBe('suggest route obvious broccoli good position hidden tone history around final lobster');
expect(parsed.isTestnet).toBe(false);
expect(parsed.import.derivationPath).toBe("m/44'/0'/0'");
expect(parsed.import.hasPassphrase).toBe(false);
});
it ('import BCH wallet with passphrase', function() {
var parsed = bitcoinUriService.parse("1|fringe hazard all hobby trap myth fire stand sock empty soon east|livenet|m/44'/0'/0'|true");
expect(parsed.isValid).toBe(true);
expect(parsed.import.type).toBe(1);
expect(parsed.import.data).toBe('fringe hazard all hobby trap myth fire stand sock empty soon east');
expect(parsed.isTestnet).toBe(false);
expect(parsed.import.derivationPath).toBe("m/44'/0'/0'");
expect(parsed.import.hasPassphrase).toBe(true);
});
it ('import BTC wallet testnet', function() {
// From copay
var parsed = bitcoinUriService.parse("1|cat wealth column firm wet sauce tornado era feature monster click eyebrow|testnet|m/44'/1'/0'|false|btc");
expect(parsed.isValid).toBe(true);
expect(parsed.import.type).toBe(1);
expect(parsed.import.data).toBe('cat wealth column firm wet sauce tornado era feature monster click eyebrow');
expect(parsed.isTestnet).toBe(true);
expect(parsed.import.derivationPath).toBe("m/44'/1'/0'");
expect(parsed.import.hasPassphrase).toBe(false);
});
// Invalid addresses from https://github.com/bitcoincashorg/bitcoincash.org/blob/master/spec/cashaddr.md
it('invalid cashAddr style 1', function() {
var parsed = bitcoinUriService.parse('prefix:x64nx6hz');
expect(parsed.isValid).toBe(false);
});
it('invalid cashAddr style 2', function() {
var parsed = bitcoinUriService.parse('p:gpf8m4h7');
expect(parsed.isValid).toBe(false);
});
it('invalid cashAddr style 3', function() {
var parsed = bitcoinUriService.parse('bitcoincash:qpzry9x8gf2tvdw0s3jn54khce6mua7lcw20ayyn');
expect(parsed.isValid).toBe(false);
});
it('invalid cashAddr style 4', function() {
var parsed = bitcoinUriService.parse('bchtest:testnetaddress4d6njnut');
expect(parsed.isValid).toBe(false);
});
it('invalid cashAddr style 5', function() {
var parsed = bitcoinUriService.parse('bchreg:555555555555555555555555555555555555555555555udxmlmrz');
expect(parsed.isValid).toBe(false);
});
it('non-string', function() {
var parsed = bitcoinUriService.parse([1, 2, 3, 4]);
expect(parsed.isValid).toBe(false);
});
it('private key encrypted with BIP38', function() {
var parsed = bitcoinUriService.parse('6PRN5nEDmX842gsBzJryPu8Tw5kcsaQq1GPLcjVQPcEStvbFAtz11JX9pX');
expect(parsed.isValid).toBe(true);
expect(parsed.privateKey.encrypted).toBe('6PRN5nEDmX842gsBzJryPu8Tw5kcsaQq1GPLcjVQPcEStvbFAtz11JX9pX');
});
it('private key for compressed pubkey mainnet', function() {
var parsed = bitcoinUriService.parse('5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ');
expect(parsed.isValid).toBe(true);
expect(parsed.privateKey.wif).toBe('5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ');
expect(parsed.isTestnet).toBe(false);
});
it('private key for compressed pubkey mainnet with wrong checksum', function() {
var parsed = bitcoinUriService.parse('5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTu');
expect(parsed.isValid).toBe(false);
});
it('private key for compressed pubkey testnet', function() {
var parsed = bitcoinUriService.parse('cNJFgo1driFnPcBdBX8BrJrpxchBWXwXCvNH5SoSkdcF6JXXwHMm');
expect(parsed.isValid).toBe(true);
expect(parsed.privateKey.wif).toBe('cNJFgo1driFnPcBdBX8BrJrpxchBWXwXCvNH5SoSkdcF6JXXwHMm');
expect(parsed.isTestnet).toBe(true);
});
it('private key for compressed pubkey testnet with wrong checksum', function() {
var parsed = bitcoinUriService.parse('cNJFgo1driFnPcBdBX8BrJrpxchBWXwXCvNH5SoSkdcF6JXXwHMM');
expect(parsed.isValid).toBe(false);
});
it('private key for uncompressed pubkey mainnet', function() {
var parsed = bitcoinUriService.parse('L18V3rAhCKEioPnJ4BHLCCsaYa8eSNFrMjNQ2EdwgeAdmBSnTMwx');
expect(parsed.isValid).toBe(true);
expect(parsed.privateKey.wif).toBe('L18V3rAhCKEioPnJ4BHLCCsaYa8eSNFrMjNQ2EdwgeAdmBSnTMwx');
expect(parsed.isTestnet).toBe(false);
});
it('private key for uncompressed pubkey mainnet with wrong checksum', function() {
var parsed = bitcoinUriService.parse('L18V3rAhCKEioPnJ4BHLCCsaYa8eSNFrMjNQ2EdwgeAdmBSnTTwx');
expect(parsed.isValid).toBe(false);
});
it('private key for uncompressed pubkey testnet', function() {
var parsed = bitcoinUriService.parse('92Pg46rUhgTT7romnV7iGW6W1gbGdeezqdbJCzShkCsYNzyyNcc');
expect(parsed.isValid).toBe(true);
expect(parsed.privateKey.wif).toBe('92Pg46rUhgTT7romnV7iGW6W1gbGdeezqdbJCzShkCsYNzyyNcc');
expect(parsed.isTestnet).toBe(true);
});
it('private key for uncompressed pubkey testnet with wrong checksum', function() {
var parsed = bitcoinUriService.parse('92Pg46rUhgTT7romnV7iGW6W1gbGdeezqdbJCzShkCsYNzyyNcC');
expect(parsed.isValid).toBe(false);
});
it('URL only, http', function() {
var parsed = bitcoinUriService.parse('http://paperwallet.bitcoin.com');
expect(parsed.isValid).toBe(true);
expect(parsed.bareUrl).toBe('http://paperwallet.bitcoin.com');
});
it('URL only, https with query', function() {
var parsed = bitcoinUriService.parse('https://purse.io/?one=two&three=four');
expect(parsed.isValid).toBe(true);
expect(parsed.bareUrl).toBe('https://purse.io/?one=two&three=four');
});
});

View file

@ -0,0 +1,79 @@
'use strict';
/**
* incomingDataService is an intermediate to redirect either to the sendFlow
* or to import/join a wallet.
*/
angular.module('copayApp.services').factory('incomingDataService', function(bitcoinUriService, $log, $state, $rootScope, scannerService, sendFlowService, gettextCatalog) {
var root = {};
root.showMenu = function(data) {
$rootScope.$broadcast('incomingDataMenu.showMenu', data);
};
root.redir = function(data, cbError) {
var parsed = bitcoinUriService.parse(data);
console.log(parsed);
$log.debug(parsed);
if (parsed.isValid) {
if (parsed.isTestnet) {
if (cbError) {
var errorMessage = gettextCatalog.getString('Testnet is not supported.');
cbError(new Error(errorMessage));
}
} else {
scannerService.pausePreview();
/**
* Strategy for the action
*/
if (parsed.copayInvitation) {
$state.go('tabs.home').then(function() {
$state.transitionTo('tabs.add.join', {
url: data
});
});
} else if (parsed.import) {
$state.go('tabs.home').then(function() {
$state.transitionTo('tabs.add.import', {
code: data
});
});
} else if (
!parsed.isValid
|| parsed.privateKey
|| (sendFlowService.state.isEmpty() && !parsed.url && !parsed.amount)
) {
root.showMenu({
original: data,
parsed: parsed
});
} else {
var state = sendFlowService.state.getClone();
state.data = data;
sendFlowService.start(state, function onError(err) {
/**
* OnError, open the menu (link not validated)
*/
root.showMenu({
original: data,
parsed: parsed
});
});
}
}
} else {
if (cbError) {
var errorMessage = gettextCatalog.getString('Data not recognised.');
cbError(new Error(errorMessage));
}
}
};
return root;
});

View file

@ -1,475 +0,0 @@
'use strict';
angular.module('copayApp.services').factory('incomingData', function($log, $state, $timeout, $ionicHistory, bitcore, bitcoreCash, $rootScope, payproService, scannerService, sendFlowService, appConfigService, popupService, gettextCatalog, bitcoinCashJsService) {
var root = {};
root.showMenu = function(data) {
$rootScope.$broadcast('incomingDataMenu.showMenu', data);
};
root.redir = function(data, serviceId, serviceData) {
var originalAddress = null;
var noPrefixInAddress = 0;
if (data.toLowerCase().indexOf('bitcoin') < 0) {
noPrefixInAddress = 1;
}
if (typeof(data) == 'string' && !(/^bitcoin(cash)?:\?r=[\w+]/).exec(data) && (data.toLowerCase().indexOf('bitcoincash:') >= 0 || data[0] == 'q' || data[0] == 'p' || data[0] == 'C' || data[0] == 'H')) {
try {
noPrefixInAddress = 0;
if (data[0] == 'p' || data[0] == 'q') {
data = 'bitcoincash:' + data;
}
var paramString = '';
if (data.indexOf('?') >= 0) {
paramString = data.substring(data.indexOf('?'));
data = data.substring(0, data.indexOf('?'));
}
if (data.indexOf('BITCOINCASH:') >= 0) {
data = data.toLowerCase();
}
originalAddress = data.replace('bitcoincash:', '');
var legacyAddress = bitcoinCashJsService.readAddress(data).legacy;
data = 'bitcoincash:' + legacyAddress + paramString;
} catch (ex) {}
}
$log.debug('Processing incoming data: ' + data);
function sanitizeUri(data) {
// Fixes when a region uses comma to separate decimals
var regex = /[\?\&]amount=(\d+([\,\.]\d+)?)/i;
var match = regex.exec(data);
if (!match || match.length === 0) {
return data;
}
var value = match[0].replace(',', '.');
var newUri = data.replace(regex, value);
// mobile devices, uris like copay://glidera
newUri.replace('://', ':');
return newUri;
}
function getParameterByName(name, url) {
if (!url) return;
name = name.replace(/[\[\]]/g, "\\$&");
var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
results = regex.exec(url);
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, " "));
}
function checkPrivateKey(privateKey) {
try {
new bitcore.PrivateKey(privateKey, 'livenet');
} catch (err) {
return false;
}
return true;
}
function goSend(addr, amount, message, coin, serviceId, serviceData) {
$state.go('tabs.send', {}, {
'reload': true,
'notify': $state.current.name == 'tabs.send' ? false : true
});
// Timeout is required to enable the "Back" button
$timeout(function() {
var params = sendFlowService.getStateClone();
if (amount) {
params.amount = amount;
}
if (addr) {
params.toAddress = addr;
params.displayAddress = originalAddress ? originalAddress : addr;
}
if (coin) {
params.coin = coin;
}
if (noPrefixInAddress) {
params.noPrefixInAddress = noPrefixInAddress;
}
if (serviceId) {
params.thirdParty = [];
params.thirdParty.id = serviceId;
params.thirdParty.data = serviceData;
sendFlowService.pushState(params);
$state.transitionTo('tabs.send.amount');
} else {
sendFlowService.pushState(params);
$state.transitionTo('tabs.send.origin');
}
}, 100);
}
// data extensions for Payment Protocol with non-backwards-compatible request
if ((/^bitcoin(cash)?:\?r=[\w+]/).exec(data)) {
var coin = data.indexOf('bitcoincash') >= 0 ? 'bch' : 'btc';
data = decodeURIComponent(data.replace(/bitcoin(cash)?:\?r=/, ''));
if (coin == 'bch') {
payproService.getPayProDetailsViaHttp(data, function onGetPayProDetailsViaHttp(err, details) {
if (err) {
var message = err.toString();
if (typeof err.data === 'string') {
// i.e. 'This invoice is no longer accepting payments'
message = gettextCatalog.getString(err.data);
}
popupService.showAlert(gettextCatalog.getString('Error'), message)
} else {
handlePayPro(details, coin);
}
});
} else {
payproService.getPayProDetails(data, coin, function onGetPayProDetails(err, details) {
if (err) {
popupService.showAlert(gettextCatalog.getString('Error'), err);
} else {
handlePayPro(details, coin);
}
});
}
return true;
}
data = sanitizeUri(data);
// Bitcoin URL
if (bitcore.URI.isValid(data)) {
var coin = 'btc';
var parsed = new bitcore.URI(data);
var addr = parsed.address ? parsed.address.toString() : '';
var message = parsed.message;
var amount = parsed.amount ? parsed.amount : '';
if (parsed.r) {
payproService.getPayProDetails(parsed.r, coin, function(err, details) {
if (err) {
if (addr && amount) goSend(addr, amount, message, coin, serviceId, serviceData);
else popupService.showAlert(gettextCatalog.getString('Error'), err);
} else handlePayPro(details, coin);
});
} else {
goSend(addr, amount, message, coin, serviceId, serviceData);
}
return true;
// Cash URI
} else if (bitcoreCash.URI.isValid(data)) {
var coin = 'bch';
var parsed = new bitcoreCash.URI(data);
var addr = parsed.address ? parsed.address.toString() : '';
var message = parsed.message;
var amount = parsed.amount ? parsed.amount : '';
// paypro not yet supported on cash
if (parsed.r) {
payproService.getPayProDetails(parsed.r, coin, function(err, details) {
if (err) {
if (addr && amount)
goSend(addr, amount, message, coin, serviceId, serviceData);
else
popupService.showAlert(gettextCatalog.getString('Error'), err);
}
handlePayPro(details, coin);
});
} else {
goSend(addr, amount, message, coin, serviceId, serviceData);
}
return true;
// Cash URI with bitcoin (btc) address version number?
} else if (bitcore.URI.isValid(data.replace(/^bitcoincash:/,'bitcoin:'))) {
$log.debug('Handling bitcoincash URI with legacy address');
var coin = 'bch';
var parsed = new bitcore.URI(data.replace(/^bitcoincash:/,'bitcoin:'));
var oldAddr = parsed.address ? parsed.address.toString() : '';
if (!oldAddr) return false;
var addr = '';
var a = bitcore.Address(oldAddr).toObject();
addr = bitcoreCash.Address.fromObject(a).toString();
// Translate address
$log.debug('address transalated to:' + addr);
popupService.showConfirm(
gettextCatalog.getString('Bitcoin cash Payment'),
gettextCatalog.getString('Payment address was translated to new Bitcoin Cash address format: ' + addr),
gettextCatalog.getString('OK'),
gettextCatalog.getString('Cancel'),
function(ret) {
if (!ret) return false;
var message = parsed.message;
var amount = parsed.amount ? parsed.amount : '';
// paypro not yet supported on cash
if (parsed.r) {
payproService.getPayProDetails(parsed.r, coin, function(err, details) {
if (err) {
if (addr && amount)
goSend(addr, amount, message, coin, serviceId, serviceData);
else
popupService.showAlert(gettextCatalog.getString('Error'), err);
}
handlePayPro(details, coin);
});
} else {
goSend(addr, amount, message, coin, serviceId, serviceData);
}
}
);
return true;
// Plain URL
} else if (/^https?:\/\//.test(data)) {
payproService.getPayProDetails(data, coin, function(err, details) {
if (err) {
if ($state.includes('tabs.scan')) {
root.showMenu({
data: data,
type: 'url'
});
}
return;
}
handlePayPro(details);
return true;
});
// Plain Address
} else if (bitcore.Address.isValid(data, 'livenet') || bitcore.Address.isValid(data, 'testnet')) {
if ($state.includes('tabs.scan')) {
root.showMenu({
data: data,
type: 'bitcoinAddress'
});
} else {
goToAmountPage(data);
}
} else if (bitcoreCash.Address.isValid(data, 'livenet')) {
if ($state.includes('tabs.scan')) {
root.showMenu({
data: data,
type: 'bitcoinAddress',
coin: 'bch',
});
} else {
goToAmountPage(data, 'bch');
}
} else if (data && data.indexOf(appConfigService.name + '://glidera') === 0) {
var code = getParameterByName('code', data);
$ionicHistory.nextViewOptions({
disableAnimate: true
});
$state.go('tabs.home', {}, {
'reload': true,
'notify': $state.current.name == 'tabs.home' ? false : true
}).then(function() {
$ionicHistory.nextViewOptions({
disableAnimate: true
});
$state.transitionTo('tabs.buyandsell.glidera', {
code: code
});
});
return true;
} else if (data && data.indexOf(appConfigService.name + '://coinbase') === 0) {
var code = getParameterByName('code', data);
$ionicHistory.nextViewOptions({
disableAnimate: true
});
$state.go('tabs.home', {}, {
'reload': true,
'notify': $state.current.name == 'tabs.home' ? false : true
}).then(function() {
$ionicHistory.nextViewOptions({
disableAnimate: true
});
$state.transitionTo('tabs.buyandsell.coinbase', {
code: code
});
});
return true;
// BitPayCard Authentication
} else if (data && data.indexOf(appConfigService.name + '://') === 0) {
// Disable BitPay Card
if (!appConfigService._enabledExtensions.debitcard) return false;
var secret = getParameterByName('secret', data);
var email = getParameterByName('email', data);
var otp = getParameterByName('otp', data);
var reason = getParameterByName('r', data);
$state.go('tabs.home', {}, {
'reload': true,
'notify': $state.current.name == 'tabs.home' ? false : true
}).then(function() {
switch (reason) {
default:
case '0':
/* For BitPay card binding */
$state.transitionTo('tabs.bitpayCardIntro', {
secret: secret,
email: email,
otp: otp
});
break;
}
});
return true;
// Join
} else if (data && data.match(/^copay:[0-9A-HJ-NP-Za-km-z]{70,80}$/)) {
$state.go('tabs.home', {}, {
'reload': true,
'notify': $state.current.name == 'tabs.home' ? false : true
}).then(function() {
$state.transitionTo('tabs.add.join', {
url: data
});
});
return true;
// Old join
} else if (data && data.match(/^[0-9A-HJ-NP-Za-km-z]{70,80}$/)) {
$state.go('tabs.home', {}, {
'reload': true,
'notify': $state.current.name == 'tabs.home' ? false : true
}).then(function() {
$state.transitionTo('tabs.add.join', {
url: data
});
});
return true;
} else if (data && (data.substring(0, 2) == '6P' || checkPrivateKey(data))) {
root.showMenu({
data: data,
type: 'privateKey'
});
} else if (data && ((data.substring(0, 2) == '1|') || (data.substring(0, 2) == '2|') || (data.substring(0, 2) == '3|'))) {
$state.go('tabs.home').then(function() {
$state.transitionTo('tabs.add.import', {
code: data
});
});
return true;
} else {
if ($state.includes('tabs.scan')) {
root.showMenu({
data: data,
type: 'text'
});
}
}
return false;
};
function goToAmountPage(toAddress, coin) {
$state.go('tabs.send', {}, {
'reload': true,
'notify': $state.current.name == 'tabs.send' ? false : true
});
$timeout(function() {
var stateParams = {
toAddress: toAddress,
displayAddress: toAddress,
coin: coin,
noPrefix: 1
};
sendFlowService.pushState(stateParams);
$state.transitionTo('tabs.send.origin');
}, 100);
}
function handlePayPro(payProData, coin) {
console.log(payProData);
var toAddr = payProData.toAddress;
var amount = payProData.amount;
var paymentUrl = payProData.url;
var expires = payProData.expires;
var time = payProData.time;
if (coin === 'bch') {
var displayAddr = payProData.outputs[0].address;
toAddr = bitcoinCashJsService.readAddress('bitcoincash:' + displayAddr).legacy;
amount = payProData.outputs[0].amount;
paymentUrl = payProData.paymentUrl;
expires = Math.floor(new Date(expires).getTime() / 1000)
time = Math.ceil(new Date(time).getTime() / 1000)
}
var name = payProData.domain;
if (payProData.memo.indexOf('eGifter') > -1) {
name = 'eGifter'
} else if (paymentUrl.indexOf('https://bitpay.com') > -1) {
name = 'BitPay';
}
var thirdPartyData = {
id: 'bip70',
amount: amount,
caTrusted: true,
name: name,
domain: payProData.domain,
expires: expires,
memo: payProData.memo,
network: 'livenet',
requiredFeeRate: payProData.requiredFeeRate,
selfSigned: 0,
time: time,
displayAddress: displayAddr,
toAddress: toAddr,
url: paymentUrl,
verified: true
};
var stateParams = {
amount: thirdPartyData.amount,
toAddress: thirdPartyData.toAddress,
coin: coin,
thirdParty: thirdPartyData
};
// fee
if (thirdPartyData.requiredFeeRate) {
stateParams.requiredFeeRate = thirdPartyData.requiredFeeRate * 1024;
}
// This does not make sense, thirdPartyData gets added by stateParams below
//sendFlowService.pushState(thirdPartyData);
scannerService.pausePreview();
$state.go('tabs.send', {}, {
'reload': true,
'notify': $state.current.name == 'tabs.send' ? false : true
}).then(function() {
$timeout(function() {
sendFlowService.pushState(stateParams); // Need to do more here
$state.transitionTo('tabs.send.origin');
});
});
}
return root;
});

View file

@ -0,0 +1,180 @@
'use strict';
(function() {
angular
.module('bitcoincom.services')
.factory('latestReleaseService', latestReleaseService);
function latestReleaseService($log, $http, $ionicPopup, configService, externalLinkService, gettextCatalog, platformInfo) {
var service = {
// Functions
checkLatestRelease: checkLatestRelease,
requestLatestRelease: requestLatestRelease,
showUpdatePopup: showUpdatePopup
};
return service;
function checkLatestRelease(cb) {
var releaseURL = configService.getDefaults().release.url;
requestLatestRelease(releaseURL, function (err, releaseData) {
if (err) return cb(err);
var currentVersion = window.version;
var latestVersion = releaseData.tag_name;
if (!verifyTagFormat(currentVersion))
return cb('Cannot verify the format of version tag: ' + currentVersion);
if (!verifyTagFormat(latestVersion))
return cb('Cannot verify the format of latest release tag: ' + latestVersion);
var current = formatTagNumber(currentVersion);
var latest = formatTagNumber(latestVersion);
if (latest.major < current.major || (latest.major === current.major && latest.minor <= current.minor)) {
return cb(null, false);
}
var releaseSearchTerm = "";
if (platformInfo.isNW) { // XX SP: DESKTOP: Check if the latest release is already available for current OS
var platform = process.platform;
if (platform === "darwin") {
releaseSearchTerm = "osx";
} else if (platform === "win32") {
releaseSearchTerm = "win";
} else if (platform === "linux") {
releaseSearchTerm = "linux";
}
var foundNewVersion = false;
for (var i in releaseData.assets) {
if (releaseData.assets[i].name.indexOf(releaseSearchTerm) !== -1) {
foundNewVersion = true;
break;
}
}
}
$log.debug('A new version is available: ' + latestVersion);
var releaseNotes = false;
if (releaseData.body) {
var releaseLines = releaseData.body.split('\n');
for (var lineNum in releaseLines) {
if (releaseLines[lineNum].substring(0, 2) === "# ") {
releaseLines[lineNum] = "<strong>" + releaseLines[lineNum].substring(2) + "</strong>";
} else if (releaseLines[lineNum].substring(0, 2) === "- ") {
releaseLines[lineNum] = "&bull; " + releaseLines[lineNum].substring(2);
}
}
releaseNotes = releaseLines.join('\n');
}
return cb(null, {latestVersion: latestVersion, releaseNotes: releaseNotes});
});
function verifyTagFormat(tag) {
var regex = /^v?\d+\.\d+(\.\d+)?(-rc\d)?$/i;
return regex.exec(tag);
}
function formatTagNumber(tag) {
var label = false;
if (tag.split("-")[1]) { // Move postfixes like "-rc2" to a variable
label = tag.split("-")[1];
tag = tag.split("-")[0];
}
var formattedNumber = tag.replace(/^v/i, '').split('.');
return {
major: +(formattedNumber[0] ? +formattedNumber[0] : 0),
minor: +(formattedNumber[1] ? +formattedNumber[1] : 0),
patch: +(formattedNumber[2] ? +formattedNumber[2] : 0),
label: label /* XX SP: Maybe we can use this in a later stage (with for example 1.0.0-rc2 the value will be "rc2" and false if there is no label) */
};
}
}
function requestLatestRelease(releaseURL, cb) {
$log.debug('Retrieving latest release information...');
var request = {
url: releaseURL,
method: 'GET',
json: true
};
$http(request).then(function (release) {
$log.debug('Latest release: ' + release.data.name);
return cb(null, release.data);
}, function (err) {
return cb('Cannot get the release information: ' + err);
});
}
function showUpdatePopup() {
var buttons = [];
if (!platformInfo.isIOS) { // There is no GitHub-release for iPhone
buttons.push({
text: "GitHub",
type: 'button-positive',
onTap: function () {
var url = 'https://github.com/Bitcoin-com/Wallet/releases/latest';
externalLinkService.open(url, false);
}
});
}
if (platformInfo.isAndroid) {
buttons.unshift({
text: "Google Play Store",
type: 'button-positive',
onTap: function () {
var url = 'https://play.google.com/store/apps/details?id=com.bitcoin.mwallet';
externalLinkService.open(url, false);
}
});
}
if (platformInfo.isIOS) {
buttons.unshift({
text: "App Store",
type: 'button-positive',
onTap: function () {
var url = 'https://itunes.apple.com/app/id1252903728';
externalLinkService.open(url, false);
}
});
} else if (platformInfo.isNW) {
if (process.platform === 'darwin') {
buttons.unshift({
text: "Mac App Store",
type: 'button-positive',
onTap: function () {
var url = 'https://itunes.apple.com/app/bitcoin-com-wallet/id1383072453';
externalLinkService.open(url, false);
}
});
}
}
if (buttons.length === 1) { // There is only one source to download (probably on desktop, so open GitHub release page..)
buttons[0].onTap();
} else {
buttons.push({
text: gettextCatalog.getString('Go Back'),
type: 'button-positive',
onTap: function () {
return true;
}
});
$ionicPopup.show({
title: gettextCatalog.getString('Update Available'),
subTitle: gettextCatalog.getString('An update to this app is available. For your security, please update to the latest version.'),
cssClass: 'popup-update',
buttons: buttons
});
}
}
}
})();

View file

@ -1,63 +0,0 @@
'use strict';
angular.module('copayApp.services')
.factory('latestReleaseService', function latestReleaseServiceFactory($log, $http, configService) {
var root = {};
root.checkLatestRelease = function(cb) {
var releaseURL = configService.getDefaults().release.url;
requestLatestRelease(releaseURL, function(err, release) {
if (err) return cb(err);
var currentVersion = window.version;
var latestVersion = release.data.tag_name;
if (!verifyTagFormat(currentVersion))
return cb('Cannot verify the format of version tag: ' + currentVersion);
if (!verifyTagFormat(latestVersion))
return cb('Cannot verify the format of latest release tag: ' + latestVersion);
var current = formatTagNumber(currentVersion);
var latest = formatTagNumber(latestVersion);
if (latest.major < current.major || (latest.major == current.major && latest.minor <= current.minor))
return cb(null, false);
$log.debug('A new version is available: ' + latestVersion);
return cb(null, true);
});
function verifyTagFormat(tag) {
var regex = /^v?\d+\.\d+\.\d+$/i;
return regex.exec(tag);
};
function formatTagNumber(tag) {
var formattedNumber = tag.replace(/^v/i, '').split('.');
return {
major: +formattedNumber[0],
minor: +formattedNumber[1],
patch: +formattedNumber[2]
};
};
};
function requestLatestRelease(releaseURL, cb) {
$log.debug('Retrieving latest relsease information...');
var request = {
url: releaseURL,
method: 'GET',
json: true
};
$http(request).then(function(release) {
$log.debug('Latest release: ' + release.data.name);
return cb(null, release);
}, function(err) {
return cb('Cannot get the release information: ' + err);
});
};
return root;
});

View file

@ -52,11 +52,7 @@ angular.module('copayApp.services').factory('ongoingProcess', function($log, $ti
root.clear = function() {
ongoingProcess = {};
if (isCordova && !isWindowsPhoneApp) {
window.plugins.spinnerDialog.hide();
} else {
$ionicLoading.hide();
}
$ionicLoading.hide();
};
root.get = function(processName) {
@ -82,23 +78,14 @@ angular.module('copayApp.services').factory('ongoingProcess', function($log, $ti
if (customHandler) {
customHandler(processName, showName, isOn);
} else if (root.onGoingProcessName) {
if (isCordova && !isWindowsPhoneApp) {
window.plugins.spinnerDialog.show(null, showName, root.clear);
} else {
var tmpl;
if (isWindowsPhoneApp) tmpl = '<div>' + showName + '</div>';
else tmpl = '<div class="item-icon-left">' + showName + '<ion-spinner class="spinner-stable" icon="lines"></ion-spinner></div>';
$ionicLoading.show({
template: tmpl
});
}
var tmpl;
if (isWindowsPhoneApp) tmpl = '<div>' + showName + '</div>';
else tmpl = '<div class="item-icon-left">' + showName + '<ion-spinner class="spinner-stable" icon="lines"></ion-spinner></div>';
$ionicLoading.show({
template: tmpl,
});
} else {
if (isCordova && !isWindowsPhoneApp) {
window.plugins.spinnerDialog.hide();
} else {
$ionicLoading.hide();
}
$ionicLoading.hide();
}
};

View file

@ -1,6 +1,6 @@
'use strict';
angular.module('copayApp.services').factory('openURLService', function($rootScope, $ionicHistory, $document, $log, $state, platformInfo, lodash, profileService, incomingData, appConfigService) {
angular.module('copayApp.services').factory('openURLService', function($rootScope, $ionicHistory, $document, $log, $state, platformInfo, lodash, profileService, incomingDataService, appConfigService) {
var root = {};
var handleOpenURL = function(args) {
@ -23,9 +23,12 @@ angular.module('copayApp.services').factory('openURLService', function($rootScop
document.addEventListener('handleopenurl', handleOpenURL, false);
if (!incomingData.redir(url)) {
$log.warn('Unknown URL! : ' + url);
}
incomingDataService.redir(url, function onError(err) {
if (err) {
$log.warn('Unknown URL! : ' + url);
popupService.showAlert(gettextCatalog.getString('Error'), err.message);
}
});
};
var handleResume = function() {

View file

@ -0,0 +1,85 @@
'use strict';
(function(){
angular
.module('bitcoincom.services')
.factory('sendFlowRouterService', sendFlowRouterService);
function sendFlowRouterService(
sendFlowStateService
, $state, $ionicHistory, $timeout
) {
var service = {
// Functions
start: start,
goNext: goNext,
goBack: goBack,
};
return service;
/**
* Start new send flow
*/
function start() {
var state = sendFlowStateService.state;
if (state.isRequestAmount) {
$state.go('tabs.paymentRequest.amount');
} else {
if ($state.current.name != 'tabs.send') {
$state.go('tabs.home').then(function () {
$ionicHistory.clearHistory();
$state.go('tabs.send').then(function () {
$timeout(function () {
goNext();
}, 60);
});
});
} else {
goNext();
}
}
}
/**
* Go to the next page
* Routing strategy : https://bitcoindotcom.atlassian.net/wiki/x/BQDWKQ
*/
function goNext() {
var state = sendFlowStateService.state;
var needsDestination = !state.toWalletId && !state.toAddress;
var needsOrigin = !state.fromWalletId;
var needsAmount = !state.amount && !state.sendMax;
if (needsDestination) {
if (!state.isWalletTransfer && !state.thirdParty) {
$state.go('tabs.send');
return;
} else if (!needsOrigin) {
$state.go('tabs.send.destination');
return;
}
}
if (needsOrigin) {
$state.go('tabs.send.origin');
} else if (needsAmount) {
$state.go('tabs.send.amount');
} else {
$state.go('tabs.send.review');
}
}
/**
* Go to the previous page
*/
function goBack() {
$ionicHistory.goBack();
}
};
})();

View file

@ -0,0 +1,142 @@
'use strict';
(function(){
angular
.module('bitcoincom.services')
.factory('sendFlowStateService', sendFlowStateService);
function sendFlowStateService($log) {
var service = {
// Variables
state: {
amount: 0,
displayAddress: null,
fromWalletId: '',
sendMax: false,
thirdParty: null,
toAddress: '',
toWalletId: '',
coin: '',
isRequestAmount: false,
isWalletTransfer: false
},
previousStates: [],
// Functions
init: init,
clear: clear,
getClone: getClone,
map: map,
pop: pop,
push: push,
isEmpty: isEmpty
};
return service;
/**
* Init state & stack
* @param {Object} params
*/
function init(params) {
$log.debug("send-flow-state init()");
clear();
if (params) {
push(params);
}
}
/**
* Clear a state & stack
*/
function clear() {
$log.debug("send-flow-state clear()");
clearCurrent();
service.previousStates = [];
}
/**
* Clear current state only
*/
function clearCurrent() {
$log.debug("send-flow-state clearCurrent()");
service.state = {
amount: 0,
displayAddress: null,
fromWalletId: '',
sendMax: false,
thirdParty: null,
toAddress: '',
toWalletId: '',
coin: '',
isRequestAmount: false,
isWalletTransfer: false
}
}
/**
* Get a clone of the current state
*/
function getClone() {
var currentState = {};
Object.keys(service.state).forEach(function forCurrentParam(key) {
if (typeof service.state[key] !== 'function' && key !== 'previousStates') {
currentState[key] = service.state[key];
}
});
return currentState;
}
/**
* Fill in the current state from the params
* @param {Object} params
*/
function map(params) {
Object.keys(params).forEach(function forNewParam(key) {
service.state[key] = params[key];
});
}
/**
* Pop state
*/
function pop() {
$log.debug('send-flow-state pop');
if (service.previousStates.length) {
var params = service.previousStates.pop();
clearCurrent();
map(params);
} else {
clear();
}
}
/**
* Push state
* @param {Object} params
*/
function push(params) {
$log.debug('send-flow-state push');
var currentParams = getClone();
service.previousStates.push(currentParams);
clearCurrent();
map(params);
}
/**
* Is empty stack
*/
function isEmpty() {
return service.previousStates.length == 0;
}
};
})();

View file

@ -0,0 +1,148 @@
'use strict';
(function(){
angular
.module('bitcoincom.services')
.factory('sendFlowService', sendFlowService);
function sendFlowService(
sendFlowStateService, sendFlowRouterService
, bitcoinUriService, payproService, bitcoinCashJsService
, popupService, gettextCatalog
, $state, $log
) {
var service = {
// Variables
state: sendFlowStateService,
router: sendFlowRouterService,
// Functions
start: start,
goNext: goNext,
goBack: goBack
};
return service;
/**
* Start a new send flow
* @param {Object} params
* @param {Function} onError
*/
function start(params, onError) {
$log.debug('send-flow start()');
if (params && params.data) {
var res = bitcoinUriService.parse(params.data);
if (res.isValid) {
// If BIP70 (url)
if (res.url) {
var url = res.url;
var coin = res.coin || '';
payproService.getPayProDetails(url, coin, function onGetPayProDetails(err, payProData) {
if (err) {
popupService.showAlert(gettextCatalog.getString('Error'), err);
} else {
var name = payProData.domain;
// Detect some merchant that we know
if (payProData.memo.indexOf('eGifter') > -1) {
name = 'eGifter'
} else if (paymentUrl.indexOf('https://bitpay.com') > -1) {
name = 'BitPay';
}
// Init thirdParty
var thirdPartyData = {
id: 'bip70',
caTrusted: true,
name: name,
domain: payProData.domain,
expires: payProData.expires,
memo: payProData.memo,
network: 'livenet',
requiredFeeRate: payProData.requiredFeeRate,
selfSigned: 0,
time: payProData.time,
url: payProData.url,
verified: true
};
// Fill in params
params.amount = payProData.amount,
params.toAddress = payProData.toAddress,
params.coin = coin,
params.thirdParty = thirdPartyData
}
// Resolve
_next();
});
} else {
if (res.coin) {
params.coin = res.coin;
}
if (res.amountInSatoshis) {
params.amount = res.amountInSatoshis;
}
if (res.publicAddress) {
var prefix = res.isTestnet ? 'bchtest:' : 'bitcoincash:';
params.displayAddress = res.publicAddress.cashAddr || res.publicAddress.legacy || res.publicAddress.bitpay;
var formatAddress = res.publicAddress.cashAddr ? prefix + params.displayAddress : params.displayAddress;
params.toAddress = bitcoinCashJsService.readAddress(formatAddress).legacy;
}
_next();
}
} else {
if (onError) {
onError();
}
}
} else {
_next();
}
// Next used for sync the async task
function _next() {
sendFlowStateService.init(params);
// Routing strategy to -> send-flow-router.service
sendFlowRouterService.start();
}
}
/**
* Go to the next step
* @param {Object} state
*/
function goNext(state) {
$log.debug('send-flow goNext()');
// Save the current route before leaving
state.route = $state.current.name;
// Save the state and redirect the user
sendFlowStateService.push(state);
sendFlowRouterService.goNext();
}
/**
* Go to the previous step
*/
function goBack() {
$log.debug('send-flow goBack()');
// Remove the state on top and redirect the user
sendFlowStateService.pop();
sendFlowRouterService.goBack();
}
}
})();

View file

@ -1,103 +0,0 @@
'use strict';
(function(){
angular
.module('copayApp.services')
.factory('sendFlowService', sendFlowService);
function sendFlowService($log) {
var service = {
// A separate state variable so we can ensure it is cleared of everything,
// even other properties added that this service does not know about. (such as "coin")
state: {
amount: '',
displayAddress: null,
fromWalletId: '',
sendMax: false,
thirdParty: null,
toAddress: '',
toWalletId: ''
},
previousStates: [],
// Functions
clear: clear,
getStateClone: getStateClone,
map: map,
popState: popState,
pushState: pushState,
startSend: startSend
};
return service;
function clear() {
console.log("sendFlow clear()");
clearCurrent();
service.previousStates = [];
}
function clearCurrent() {
console.log("sendFlow clearCurrent()");
service.state = {
amount: '',
displayAddress: null,
fromWalletId: '',
sendMax: false,
thirdParty: null,
toAddress: '',
toWalletId: ''
}
}
/**
* Handy for debugging
*/
function getStateClone() {
var currentState = {};
Object.keys(service.state).forEach(function forCurrentParam(key) {
if (typeof service.state[key] !== 'function' && key !== 'previousStates') {
currentState[key] = service.state[key];
}
});
return currentState;
}
/**
* Clears all previous state
*/
function startSend(params) {
console.log('startSend()');
clear();
map(params);
}
function map(params) {
Object.keys(params).forEach(function forNewParam(key) {
service.state[key] = params[key];
});
};
function popState() {
console.log('sendFlow pop');
if (service.previousStates.length) {
var params = service.previousStates.pop();
clearCurrent();
map(params);
} else {
clear();
}
};
function pushState(params) {
console.log('sendFlow push');
var currentParams = getStateClone();
service.previousStates.push(currentParams);
clearCurrent();
map(params);
};
};
})();

View file

@ -328,18 +328,23 @@ angular.module('copayApp.services').factory('shapeshiftApiService', function($q)
$scope.amount, $scope.withdrawalAddress,
$scope.coinIn, $scope.coinOut
);
console.log('shapeshiftApiService.FixedAmountTx()');
console.log(fixedTx);
SSA.FixedAmountTx(fixedTx, function (data) {
console.log(data)
return promise.resolve({ fixedTxData : data.success });
console.log(data);
promise.resolve(data);
});
return promise.promise;
},
NormalTx : function($scope){
var promise = $q.defer();
var normalTx = SSA.CreateNormalTx($scope.withdrawalAddress, $scope.coinIn, $scope.coinOut);
console.log('shapeshiftApiService.NormalTx()');
console.log(normalTx);
SSA.NormalTx(normalTx, function (data) {
promise.resolve({ normalTxData : data });
console.log(data);
promise.resolve(data);
});
return promise.promise;
},
@ -360,11 +365,12 @@ angular.module('copayApp.services').factory('shapeshiftApiService', function($q)
return promise.promise;
},
ValidateAddress : function(address, coin) {
var promise = $q.defer();
SSA.ValidateAdddress(address, coin, function(data){
promise.resolve(data);
});
return promise.promise;
var promise = $q.defer();
SSA.ValidateAdddress(address, coin, function onRequest(data){
console.log(data);
promise.resolve(data);
});
return promise.promise;
}
};
});

View file

@ -0,0 +1,112 @@
'use strict';
(function(){
angular
.module('bitcoincom.services')
.factory('shapeshiftService', shapeshiftService);
function shapeshiftService(shapeshiftApiService, gettextCatalog) {
var service = {
// Variables
coinIn: '',
coinOut: '',
withdrawalAddress: '',
returnAddress: '',
amount: '',
marketData: {},
coins: {
'BTC': {name: 'Bitcoin', symbol: 'BTC'},
'BCH': {name: 'Bitcoin Cash', symbol: 'BCH'}
},
// Functions
getMarketData: getMarketData,
shiftIt: shiftIt
};
return service;
function handleError(response, defaultMessage, cb) {
if (response && typeof response.error === "string") {
cb(new Error(response.error));
} else if (response && response.error && response.error.message) {
cb(new Error(response.error.message));
} else {
cb(new Error(defaultMessage));
}
}
function getMarketData(coinIn, coinOut, cb) {
service.coinIn = coinIn;
service.coinOut = coinOut;
shapeshiftApiService
.marketInfo(service.coinIn, service.coinOut)
.then(function (response) {
if (!response || response.error) {
handleError(response, 'Invalid response from Shapeshift', cb);
} else {
service.marketData = response;
service.rateString = service.marketData.rate.toString() + ' ' + coinOut.toUpperCase() + '/' + coinIn.toUpperCase();
cb(null, response);
}
});
}
function shiftIt(coinIn, coinOut, withdrawalAddress, returnAddress, amount, cb) {
// Test if the amount is correct depending on the min and max
if (!amount || typeof amount !== 'number') {
cb(new Error(gettextCatalog.getString('Amount is not defined')));
} else if (amount < service.marketData.minimum) {
cb(new Error(gettextCatalog.getString('Amount is below the minimun')));
} else if (amount > service.marketData.maxLimit) {
cb(new Error(gettextCatalog.getString('Amount is above the limit')));
} else {
// Init service data
service.withdrawalAddress = withdrawalAddress;
service.returnAddress = returnAddress;
service.coinIn = coinIn;
service.coinOut = coinOut;
service.amount = amount;
// Check the address
shapeshiftApiService
.ValidateAddress(withdrawalAddress, coinOut)
.then(function onSuccess(response) {
if (response && response.isvalid) {
// Prepare the transaction shapeshift side
shapeshiftApiService.NormalTx(service).then(function onResponse(response) {
// If error, return it
if (!response || response.error) {
handleError(response, gettextCatalog.getString('Invalid response from Shapeshift'), cb);
} else {
var txData = response;
// If the content is not that it was expected, get back an error
if (!txData || !txData.orderId || !txData.deposit) {
cb(new Error(gettextCatalog.getString('Invalid response from Shapeshift')));
} else {
// Get back the data
service.depositInfo = txData;
var shapeshiftData = {
coinIn: coinIn,
coinOut: coinOut,
toWalletId: service.toWalletId,
minAmount: service.marketData.minimum,
maxAmount: service.marketData.maxLimit,
orderId: txData.orderId,
toAddress: txData.deposit
};
cb(null, shapeshiftData);
}
}
});
} else {
cb(new Error(gettextCatalog.getString('Invalid address')));
}
});
}
}
}
})();

View file

@ -1,141 +0,0 @@
'use strict';
angular.module('copayApp.services').factory('shapeshiftService', function ($http, $interval, $log, lodash, moment, ongoingProcess, shapeshiftApiService, storageService, configService, incomingData, platformInfo, servicesService) {
var root = {};
root.ShiftState = 'Shift';
root.coinIn = '';
root.coinOut = '';
root.withdrawalAddress = '';
root.returnAddress = '';
root.amount = '';
root.marketData = {};
root.getMarketDataIn = function (coin) {
if (coin === root.coinOut) return root.getMarketData(root.coinOut, root.coinIn);
return root.getMarketData(coin, root.coinOut);
};
root.getMarketDataOut = function (coin) {
if (coin === root.coinIn) return root.getMarketData(root.coinOut, root.coinIn);
return root.getMarketData(root.coinIn, coin);
};
root.getMarketData = function (coinIn, coinOut, cb) {
root.coinIn = coinIn;
root.coinOut = coinOut;
if (root.coinIn === undefined || root.coinOut === undefined) return;
shapeshiftApiService
.marketInfo(root.coinIn, root.coinOut)
.then(function (marketData) {
root.marketData = marketData;
root.rateString = root.marketData.rate.toString() + ' ' + coinOut.toUpperCase() + '/' + coinIn.toUpperCase();
if (cb) {
cb(marketData);
}
});
};
/*shapeshiftApiService.coins().then(function(coins){
root.coins = coins;
root.coinIn = coins['BTC'].symbol;
root.coinOut = coins['BCH'].symbol;
root.getMarketData(root.coinIn, root.coinOut);
});*/
root.coins = {
'BTC': {name: 'Bitcoin', symbol: 'BTC'},
'BCH': {name: 'Bitcoin Cash', symbol: 'BCH'}
};
function checkForError(data) {
if (data.err) return true;
return false;
}
root.shiftIt = function (coinIn, coinOut, withdrawalAddress, returnAddress, cb) {
ongoingProcess.set('connectingShapeshift', true);
root.withdrawalAddress = withdrawalAddress;
root.returnAddress = returnAddress;
root.coinIn = coinIn;
root.coinOut = coinOut;
shapeshiftApiService.ValidateAddress(withdrawalAddress, coinOut).then(function (valid) {
var tx = ShapeShift();
var coin;
console.log("Starting");
tx.then(function (txData) {
console.log("Got txData", txData);
if (txData['fixedTxData']) {
txData = txData.fixedTxData;
if (checkForError(txData)) return cb(txData.err);
//console.log(txData)
var coinPair = txData.pair.split('_');
txData.depositType = coinPair[0].toUpperCase();
txData.withdrawalType = coinPair[1].toUpperCase();
coin = root.coins[txData.depositType].name.toLowerCase();
txData.depositQR = coin + ":" + txData.deposit + "?amount=" + txData.depositAmount;
root.txFixedPending = true;
} else if (txData['normalTxData']) {
txData = txData.normalTxData;
if (checkForError(txData)) return cb(txData.err);
coin = root.coins[txData.depositType.toUpperCase()].name.toLowerCase();
txData.depositQR = coin + ":" + txData.deposit;
} else if (txData['cancelTxData']) {
txData = txData.cancelTxData;
if (checkForError(txData)) return cb(txData.err);
if (root.txFixedPending) {
root.txFixedPending = false;
}
root.ShiftState = 'Shift';
}
root.depositInfo = txData;
//console.log(root.marketData);
//console.log(root.depositInfo);
var sendAddress = txData.depositQR;
if (sendAddress && sendAddress.indexOf('bitcoin cash') >= 0)
sendAddress = sendAddress.replace('bitcoin cash', 'bitcoincash');
ongoingProcess.set('connectingShapeshift', false);
root.ShiftState = 'Cancel';
//root.GetStatus();
//root.txInterval=$interval(root.GetStatus, 8000);
var shapeshiftData = {
coinIn: coinIn,
coinOut: coinOut,
toWalletId: root.toWalletId,
minAmount: root.marketData.minimum,
maxAmount: root.marketData.maxLimit,
orderId: root.depositInfo.orderId,
toAddress: txData.deposit
};
//
// if (incomingData.redir(sendAddress, 'shapeshift', shapeshiftData)) {
ongoingProcess.set('connectingShapeshift', false);
// return;
// }
cb(null, shapeshiftData);
});
})
};
function ShapeShift() {
if (parseFloat(root.amount) > 0) return shapeshiftApiService.FixedAmountTx(root);
return shapeshiftApiService.NormalTx(root);
}
root.GetStatus = function () {
var address = root.depositInfo.deposit
shapeshiftApiService.GetStatusOfDepositToAddress(address).then(function (data) {
root.DepositStatus = data;
if (root.DepositStatus.status === 'complete') {
$interval.cancel(root.txInterval);
root.depositInfo = null;
root.ShiftState = 'Shift'
}
});
};
return root;
});