This commit is contained in:
Matias Alejo Garcia 2015-03-06 12:00:10 -03:00
commit 320de62f13
348 changed files with 7745 additions and 30874 deletions

View file

@ -0,0 +1,24 @@
'use strict';
angular.module('copayApp.services')
.factory('applicationService', function($rootScope, $timeout, isCordova) {
var root = {};
root.restart = function(hard) {
if (isCordova) {
$rootScope.iden = $rootScope.wallet = undefined;
if (hard) {
location.reload();
}
$timeout(function() {
$rootScope.$digest();
}, 1);
} else {
// Go home reloading the application
var hashIndex = window.location.href.indexOf('#!/');
window.location = window.location.href.substr(0, hashIndex);
}
};
return root;
});

View file

@ -0,0 +1,83 @@
'use strict';
angular.module('copayApp.services')
.factory('backupService', function backupServiceFactory($log, $timeout, profileService, sjcl) {
var root = {};
var _download = function(ew, filename, cb) {
var NewBlob = function(data, datatype) {
var out;
try {
out = new Blob([data], {
type: datatype
});
$log.debug("case 1");
} catch (e) {
window.BlobBuilder = window.BlobBuilder ||
window.WebKitBlobBuilder ||
window.MozBlobBuilder ||
window.MSBlobBuilder;
if (e.name == 'TypeError' && window.BlobBuilder) {
var bb = new BlobBuilder();
bb.append(data);
out = bb.getBlob(datatype);
$log.debug("case 2");
} else if (e.name == "InvalidStateError") {
// InvalidStateError (tested on FF13 WinXP)
out = new Blob([data], {
type: datatype
});
$log.debug("case 3");
} else {
// We're screwed, blob constructor unsupported entirely
$log.debug("Errore");
}
}
return out;
};
var a = document.createElement("a");
document.body.appendChild(a);
a.style = "display: none";
var blob = new NewBlob(ew, 'text/plain;charset=utf-8');
var url = window.URL.createObjectURL(blob);
a.href = url;
a.download = filename;
a.click();
$timeout(function() {
window.URL.revokeObjectURL(url);
}, 250);
return cb();
};
root.walletExport = function(password) {
if (!password) {
return null;
}
var fc = profileService.focusedClient;
try {
var b = fc.export({});
var e = sjcl.encrypt(password, b, {
iter: 10000
});
return e;
} catch (err) {
$log.debug('Error exporting wallet: ', err);
return null;
};
};
root.walletDownload = function(password, cb) {
var fc = profileService.focusedClient;
var ew = root.walletExport(password);
if (!ew) return cb('Could not create backup');
var walletName = fc.credentials.walletName;
var filename = walletName + '-Copaybackup.aes.json';
_download(ew, filename, cb)
};
return root;
});

View file

@ -0,0 +1,92 @@
'use strict';
angular.module('copayApp.services')
.factory('balanceService', function($rootScope, $filter, $timeout, bwcService) {
var root = {};
var _balanceCache = {};
root.clearBalanceCache = function(w) {
w.clearUnspentCache();
delete _balanceCache[w.getId()];
};
root._fetchBalance = function(w, cb) {
cb = cb || function() {};
var satToUnit = 1 / w.settings.unitToSatoshi;
var COIN = bwcService.Bitcore.util.COIN;
w.getBalance(function(err, balanceSat, balanceByAddrSat, safeBalanceSat, safeUnspentCount) {
if (err) return cb(err);
var r = {};
r.totalBalance = $filter('noFractionNumber')(balanceSat * satToUnit);
r.totalBalanceBTC = (balanceSat / COIN);
var availableBalanceNr = safeBalanceSat * satToUnit;
r.availableBalance = $filter('noFractionNumber')(safeBalanceSat * satToUnit);
r.availableBalanceBTC = (safeBalanceSat / COIN);
r.safeUnspentCount = safeUnspentCount;
var lockedBalance = (balanceSat - safeBalanceSat) * satToUnit;
r.lockedBalance = lockedBalance ? $filter('noFractionNumber')(lockedBalance) : null;
r.lockedBalanceBTC = (balanceSat - safeBalanceSat) / COIN;
if (r.safeUnspentCount) {
var estimatedFee = copay.Wallet.estimatedFee(r.safeUnspentCount);
r.topAmount = (((availableBalanceNr * w.settings.unitToSatoshi).toFixed(0) - estimatedFee) / w.settings.unitToSatoshi);
}
var balanceByAddr = {};
for (var ii in balanceByAddrSat) {
balanceByAddr[ii] = balanceByAddrSat[ii] * satToUnit;
}
r.balanceByAddr = balanceByAddr;
r.totalBalanceAlternative = $filter('noFractionNumber')(totalBalanceAlternative, 2);
r.lockedBalanceAlternative = $filter('noFractionNumber')(lockedBalanceAlternative, 2);
r.alternativeConversionRate = $filter('noFractionNumber')(alternativeConversionRate, 2);
r.alternativeBalanceAvailable = true;
r.alternativeIsoCode = w.settings.alternativeIsoCode;
r.updatingBalance = false;
return cb(null, r)
});
};
root.update = function(w, cb, isFocused) {
w = w || $rootScope.wallet;
if (!w || !w.isComplete()) return;
copay.logger.debug('Updating balance of:', w.getName(), isFocused);
var wid = w.getId();
// cache available? Set the cached values until we updated them
if (_balanceCache[wid]) {
w.balanceInfo = _balanceCache[wid];
} else {
if (isFocused)
$rootScope.updatingBalance = true;
}
w.balanceInfo = w.balanceInfo || {};
w.balanceInfo.updating = true;
root._fetchBalance(w, function(err, res) {
if (err) throw err;
w.balanceInfo = _balanceCache[wid] = res;
w.balanceInfo.updating = false;
if (isFocused) {
$rootScope.updatingBalance = false;
}
// we alwalys calltimeout because if balance is cached, we are still on the same
// execution path
if (cb) $timeout(function() {
return cb();
}, 1);
});
};
return root;
});

View file

@ -0,0 +1,6 @@
'use strict';
angular.module('copayApp.services')
.factory('bitcore', function bitcoreFactory(bwcService) {
var bitcore = bwcService.getBitcore();
return bitcore;
});

View file

@ -0,0 +1,125 @@
'use strict';
angular.module('copayApp.services').factory('configService', function(localStorageService, lodash, bwcService) {
var root = {};
var defaultConfig = {
// wallet limits
limits: {
totalCopayers: 6,
mPlusN: 100,
},
// Bitcore wallet service URL
bws: {
url: 'http://162.242.245.33:3004/bws/api',
},
// insight
insight: {
testnet: {
url: 'https://test-insight.bitpay.com:443',
transports: ['polling'],
},
livenet: {
url: 'https://insight.bitpay.com:443',
transports: ['polling'],
},
},
// wallet default config
wallet: {
requiredCopayers: 2,
totalCopayers: 3,
spendUnconfirmed: true,
reconnectDelay: 5000,
idleDurationMin: 4,
settings: {
unitName: 'bits',
unitToSatoshi: 100,
unitDecimals: 2,
unitCode: 'bit',
alternativeName: 'US Dollar',
alternativeIsoCode: 'USD',
}
},
// local encryption/security config
passphraseConfig: {
iterations: 5000,
storageSalt: 'mjuBtGybi/4=',
},
rates: {
url: 'https://insight.bitpay.com:443/api/rates',
},
};
var configCache = null;
root.getSync = function() {
if (!configCache)
throw new Error('configService#getSync called when cache is not initialized');
return configCache;
};
root.get = function(cb) {
localStorageService.get('config', function(err, localConfig) {
if (localConfig) {
configCache = JSON.parse(localConfig);
//these ifs are to avoid migration problems
if (!configCache.bws) {
configCache.bws = defaultConfig.bws;
}
if (!configCache.wallet.settings.unitCode) {
configCache.wallet.settings.unitCode = defaultConfig.wallet.settings.unitCode;
}
} else {
configCache = defaultConfig;
};
return cb(err, configCache);
});
};
root.set = function(newOpts, cb) {
var config = defaultConfig;
localStorageService.get('config', function(err, oldOpts) {
if (lodash.isString(oldOpts)) {
oldOpts = JSON.parse(oldOpts);
}
if (lodash.isString(config)) {
config = JSON.parse(config);
}
if (lodash.isString(newOpts)) {
newOpts = JSON.parse(newOpts);
}
lodash.merge(config, oldOpts, newOpts);
configCache = config;
localStorageService.set('config', JSON.stringify(config), cb);
});
};
root.reset = function(cb) {
localStorageService.remove('config', cb);
};
root.getDefaults = function() {
return defaultConfig;
};
root.get(function(err, c) {
if (err) throw Error(err);
bwcService.setBaseUrl(c.bws.url);
});
return root;
});

95
src/js/services/go.js Normal file
View file

@ -0,0 +1,95 @@
'use strict';
angular.module('copayApp.services').factory('go', function($window, $rootScope, $location, $state, profileService) {
var root = {};
var hideSidebars = function() {
if (typeof document === 'undefined')
return;
// hack to hide sidebars and use ng-click (no href=)
var win = angular.element($window);
var elem = angular.element(document.querySelector('#off-canvas-wrap'))
elem.removeClass('move-right');
elem.removeClass('move-left');
};
var toggleSidebar = function(invert) {
if (typeof document === 'undefined')
return;
var elem = angular.element(document.querySelector('#off-canvas-wrap'));
var leftbarActive = angular.element(document.getElementsByClassName('move-right')).length;
if (invert) {
if (profileService.profile && !$rootScope.hideNavigation) {
elem.addClass('move-right');
}
} else {
if (leftbarActive) {
hideSidebars();
}
}
};
root.openExternalLink = function(url) {
var ref = window.open(url, '_blank', 'location=no');
};
root.path = function(path) {
$state.transitionTo(path);
hideSidebars();
};
root.swipe = function(invert) {
toggleSidebar(invert);
};
root.walletHome = function() {
var fc = profileService.focusedClient;
if (fc && !fc.isComplete()) {
root.path('copayers');
} else {
root.path('walletHome');
}
};
root.home = function() {
if ($rootScope.iden)
root.walletHome();
else
root.path('signin');
};
root.addWallet = function() {
$state.go('add');
};
root.send = function() {
$state.go('send');
};
root.preferences = function() {
$state.go('preferences');
};
root.reload = function() {
$state.reload();
};
// Global go. This should be in a better place TODO
// We dont do a 'go' directive, to use the benefits of ng-touch with ng-click
$rootScope.go = function(path) {
root.path(path);
};
$rootScope.openExternalLink = function(url) {
root.openExternalLink(url);
};
return root;
});

View file

@ -0,0 +1,3 @@
'use strict';
angular.module('copayApp.services').value('isCordova', window.cordova ? true : false);

View file

@ -0,0 +1,29 @@
'use strict';
// Detect mobile devices
var isMobile = {
Android: function() {
return !!navigator.userAgent.match(/Android/i);
},
BlackBerry: function() {
return !!navigator.userAgent.match(/BlackBerry/i);
},
iOS: function() {
return !!navigator.userAgent.match(/iPhone|iPad|iPod/i);
},
Opera: function() {
return !!navigator.userAgent.match(/Opera Mini/i);
},
Windows: function() {
return !!navigator.userAgent.match(/IEMobile/i);
},
Safari: function() {
return Object.prototype.toString.call(window.HTMLElement).indexOf('Constructor') > 0;
},
any: function() {
return (isMobile.Android() || isMobile.BlackBerry() || isMobile.iOS() || isMobile.Opera() || isMobile.Windows());
}
};
angular.module('copayApp.services').value('isMobile', isMobile);

View file

@ -0,0 +1,139 @@
'use strict';
angular.module('copayApp.services')
.factory('legacyImportService', function($rootScope, $log, $timeout, $http, lodash, bitcore, bwcService, sjcl, profileService) {
var root = {};
var wc = bwcService.getClient();
root.getKeyForEmail = function(email) {
var hash = bitcore.crypto.Hash.sha256ripemd160(new bitcore.deps.Buffer(email)).toString('hex');
$log.debug('Storage key:' + hash);
return 'profile::' + hash;
};
root.getKeyForWallet = function(id) {
return 'wallet::' + id;
};
root._importOne = function(user, pass, walletId, get, cb) {
get(root.getKeyForWallet(walletId), function(err, blob) {
if (err) {
$log.warn('Could not fetch wallet: ' + walletId + ":" + err);
return cb('Could not fetch ' + walletId);
}
profileService.importLegacyWallet(user, pass, blob, cb);
});
};
root._doImport = function(user, pass, get, cb) {
var self = this;
get(root.getKeyForEmail(user), function(err, p) {
if (err || !p)
return cb(err || ('Could not find profile for ' + user));
var ids = wc.getWalletIdsFromOldCopay(user, pass, p);
if (!ids)
return cb('Could not find wallets on the profile');
$rootScope.$emit('Local/ImportStatusUpdate',
'Found ' + ids.length + ' wallets to import:' + ids.join());
$log.info('Importing Wallet Ids:', ids);
var i = 0;
var okIds = [];
var toScanIds = [];
lodash.each(ids, function(walletId) {
$timeout(function() {
$rootScope.$emit('Local/ImportStatusUpdate',
'Importing wallet ' + walletId + ' ... ');
self._importOne(user, pass, walletId, get, function(err, id, name, existed) {
if (err) {
$rootScope.$emit('Local/ImportStatusUpdate',
'Failed to import wallet ' + (name || walletId));
} else {
okIds.push(walletId);
$rootScope.$emit('Local/ImportStatusUpdate',
'Wallet ' + id + '[' + name + '] imported successfully');
if (!existed) {
$log.info('Wallet ' + walletId + ' was created. need to be scanned');
toScanIds.push(id);
}
}
if (++i == ids.length) {
return cb(null, okIds, toScanIds);
}
});
}, 100);
});
});
};
root.import = function(user, pass, serverURL, fromCloud, cb) {
var insightGet = function(key, cb) {
var kdfbinary = function(password, salt, iterations, length) {
iterations = iterations || defaultIterations;
length = length || 512;
salt = sjcl.codec.base64.toBits(salt || defaultSalt);
var hash = sjcl.hash.sha256.hash(sjcl.hash.sha256.hash(password));
var prff = function(key) {
return new sjcl.misc.hmac(hash, sjcl.hash.sha1);
};
return sjcl.misc.pbkdf2(hash, salt, iterations, length, prff);
};
var salt = 'jBbYTj8zTrOt6V';
var iter = 1000;
var SEPARATOR = '|';
var kdfb = kdfbinary(pass + SEPARATOR + user, salt, iter);
var kdfb64 = sjcl.codec.base64.fromBits(kdfb);
var keyBuf = new bitcore.deps.Buffer(kdfb64);
var passphrase = bitcore.crypto.Hash.sha256sha256(keyBuf).toString('base64');
var authHeader = new bitcore.deps.Buffer(user + ':' + passphrase).toString('base64');
var retrieveUrl = serverURL + '/retrieve';
var getParams = {
method: 'GET',
url: retrieveUrl + '?key=' + encodeURIComponent(key) + '&rand=' + Math.random(),
headers: {
'Authorization': authHeader,
},
};
$log.debug('Insight GET', getParams);
$http(getParams)
.success(function(data) {
data = JSON.stringify(data);
$log.info('Fetch from insight OK:' + getParams.url);
return cb(null, data);
})
.error(function() {
$log.warn('Failed to fetch from insight');
return cb('PNOTFOUND: Profile not found');
});
};
var localStorageGet = function(key, cb) {
var v = localStorage.getItem(key);
return cb(null, v);
};
var get = fromCloud ? insightGet : localStorageGet;
root._doImport(user, pass, get, cb);
};
return root;
});

View file

@ -0,0 +1,92 @@
'use strict';
angular.module('copayApp.services')
.factory('localStorageService', function() {
var isChromeApp = typeof window !== "undefined" && window.chrome && chrome.runtime && chrome.runtime.id;
var root = {};
var ls = ((typeof localStorage !== "undefined") ? localStorage : null);
if (isChromeApp && !ls) {
ls = localStorage = chrome.storage.local;
window.localStorage = chrome.storage.local;
}
if (!ls)
throw new Error('localstorage not available, cannot run plugin');
root.init = function() {};
root.get = function(k, cb) {
if (isChromeApp) {
chrome.storage.local.get(k,
function(data) {
//TODO check for errors
return cb(null, data[k]);
});
} else {
return cb(null, ls.getItem(k));
}
};
/**
* Same as setItem, but fails if an item already exists
*/
root.create = function(name, value, callback) {
root.get(name,
function(err, data) {
if (data) {
return callback('EEXISTS');
} else {
return root.set(name, value, callback);
}
});
};
root.set = function(k, v, cb) {
if (isChromeApp) {
var obj = {};
obj[k] = v;
chrome.storage.local.set(obj, cb);
} else {
ls.setItem(k, v);
return cb();
}
};
root.remove = function(k, cb) {
if (isChromeApp) {
chrome.storage.local.remove(k, cb);
} else {
ls.removeItem(k);
return cb();
}
};
root.clear = function(cb) {
// NOP
return cb();
};
root.list = function(cb) {
if (isChromeApp) {
chrome.storage.local.get(null, function(items) {
return cb(null, lodash.keys(items));
});
} else {
var ret = [];
var l = ls.length;
for (var i = 0; i < l; i++)
ret.push(ls.key(i));
return cb(null, ret);
}
};
return root;
});

View file

@ -0,0 +1,295 @@
'use strict';
angular.module('copayApp.services').
factory('notification', ['$timeout',
function($timeout) {
var notifications = [];
/*
ls.getItem('notifications', function(err, data) {
if (data) {
notifications = JSON.parse(data);
}
});
*/
var queue = [];
var settings = {
info: {
duration: 6000,
enabled: true
},
funds: {
duration: 7000,
enabled: true
},
version: {
duration: 60000,
enabled: true
},
warning: {
duration: 7000,
enabled: true
},
error: {
duration: 7000,
enabled: true
},
success: {
duration: 5000,
enabled: true
},
progress: {
duration: 0,
enabled: true
},
custom: {
duration: 35000,
enabled: true
},
details: true,
localStorage: false,
html5Mode: false,
html5DefaultIcon: 'img/favicon.ico'
};
function html5Notify(icon, title, content, ondisplay, onclose) {
if (window.webkitNotifications && window.webkitNotifications.checkPermission() === 0) {
if (!icon) {
icon = 'img/favicon.ico';
}
var noti = window.webkitNotifications.createNotification(icon, title, content);
if (typeof ondisplay === 'function') {
noti.ondisplay = ondisplay;
}
if (typeof onclose === 'function') {
noti.onclose = onclose;
}
noti.show();
} else {
settings.html5Mode = false;
}
}
return {
/* ========== SETTINGS RELATED METHODS =============*/
disableHtml5Mode: function() {
settings.html5Mode = false;
},
disableType: function(notificationType) {
settings[notificationType].enabled = false;
},
enableHtml5Mode: function() {
// settings.html5Mode = true;
settings.html5Mode = this.requestHtml5ModePermissions();
},
enableType: function(notificationType) {
settings[notificationType].enabled = true;
},
getSettings: function() {
return settings;
},
toggleType: function(notificationType) {
settings[notificationType].enabled = !settings[notificationType].enabled;
},
toggleHtml5Mode: function() {
settings.html5Mode = !settings.html5Mode;
},
requestHtml5ModePermissions: function() {
if (window.webkitNotifications) {
if (window.webkitNotifications.checkPermission() === 0) {
return true;
} else {
window.webkitNotifications.requestPermission(function() {
if (window.webkitNotifications.checkPermission() === 0) {
settings.html5Mode = true;
} else {
settings.html5Mode = false;
}
});
return false;
}
} else {
return false;
}
},
/* ============ QUERYING RELATED METHODS ============*/
getAll: function() {
// Returns all notifications that are currently stored
return notifications;
},
getQueue: function() {
return queue;
},
/* ============== NOTIFICATION METHODS ==============*/
info: function(title, content, userData) {
return this.awesomeNotify('info', 'fi-info', title, content, userData);
},
funds: function(title, content, userData) {
return this.awesomeNotify('funds', 'icon-receive', title, content, userData);
},
version: function(title, content, severe) {
return this.awesomeNotify('version', severe ? 'fi-alert' : 'fi-flag', title, content);
},
error: function(title, content, userData) {
return this.awesomeNotify('error', 'fi-x', title, content, userData);
},
success: function(title, content, userData) {
return this.awesomeNotify('success', 'fi-check', title, content, userData);
},
warning: function(title, content, userData) {
return this.awesomeNotify('warning', 'fi-alert', title, content, userData);
},
new: function(title, content, userData) {
return this.awesomeNotify('warning', 'fi-plus', title, content, userData);
},
sent: function(title, content, userData) {
return this.awesomeNotify('warning', 'icon-paperplane', title, content, userData);
},
awesomeNotify: function(type, icon, title, content, userData) {
/**
* Supposed to wrap the makeNotification method for drawing icons using font-awesome
* rather than an image.
*
* Need to find out how I'm going to make the API take either an image
* resource, or a font-awesome icon and then display either of them.
* Also should probably provide some bits of color, could do the coloring
* through classes.
*/
// image = '<i class="icon-' + image + '"></i>';
return this.makeNotification(type, false, icon, title, content, userData);
},
notify: function(image, title, content, userData) {
// Wraps the makeNotification method for displaying notifications with images
// rather than icons
return this.makeNotification('custom', image, true, title, content, userData);
},
makeNotification: function(type, image, icon, title, content, userData) {
var notification = {
'type': type,
'image': image,
'icon': icon,
'title': title,
'content': content,
'timestamp': +new Date(),
'userData': userData
};
notifications.push(notification);
if (settings.html5Mode) {
html5Notify(image, title, content, function() {
// inner on display function
}, function() {
// inner on close function
});
}
//this is done because html5Notify() changes the variable settings.html5Mode
if (!settings.html5Mode) {
queue.push(notification);
$timeout(function removeFromQueueTimeout() {
queue.splice(queue.indexOf(notification), 1);
}, settings[type].duration);
}
// Mobile notification
if (window && window.navigator && window.navigator.vibrate) {
window.navigator.vibrate([200, 100, 200]);
};
if (document.hidden && (type == 'info' || type == 'funds')) {
new window.Notification(title, {
body: content,
icon: 'img/notification.png'
});
}
this.save();
return notification;
},
/* ============ PERSISTENCE METHODS ============ */
save: function() {
// Save all the notifications into localStorage
if (settings.localStorage) {
localStorage.setItem('notifications', JSON.stringify(notifications));
}
},
restore: function() {
// Load all notifications from localStorage
},
clear: function() {
notifications = [];
this.save();
}
};
}
]).directive('notifications', function(notification, $compile) {
/**
*
* It should also parse the arguments passed to it that specify
* its position on the screen like "bottom right" and apply those
* positions as a class to the container element
*
* Finally, the directive should have its own controller for
* handling all of the notifications from the notification service
*/
function link(scope, element, attrs) {
var position = attrs.notifications;
position = position.split(' ');
element.addClass('dr-notification-container');
for (var i = 0; i < position.length; i++) {
element.addClass(position[i]);
}
}
return {
restrict: 'A',
scope: {},
templateUrl: 'views/includes/notifications.html',
link: link,
controller: ['$scope',
function NotificationsCtrl($scope) {
$scope.queue = notification.getQueue();
$scope.removeNotification = function(noti) {
$scope.queue.splice($scope.queue.indexOf(noti), 1);
};
}
]
};
});

View file

@ -0,0 +1,94 @@
'use strict';
angular.module('copayApp.services')
.factory('notificationService', function profileServiceFactory($filter, notification, lodash, configService) {
var root = {};
var groupingTime = 4000;
var lastNotificationOnWallet = {};
root.getLast = function(walletId) {
var last = lastNotificationOnWallet[walletId];
if (!last) return null;
return Date.now() - last.ts < groupingTime ? last : null;
};
root.storeLast = function(notificationData, walletId) {
lastNotificationOnWallet[walletId] = {
creatorId: notificationData.creatorId,
type: notificationData.type,
ts: Date.now(),
};
};
root.shouldSkip = function(notificationData, last) {
if (!last) return false;
// rules...
if (last.type === 'NewTxProposal'
&& notificationData.type === 'TxProposalAcceptedBy')
return true;
if (last.type === 'TxProposalFinallyAccepted'
&& notificationData.type === 'NewOutgoingTx')
return true;
if (last.type === 'TxProposalRejectedBy'
&& notificationData.type === 'TxProposalFinallyRejected')
return true;
return false;
};
root.newBWCNotification = function(notificationData, walletId, walletName) {
var last = root.getLast(walletId);
root.storeLast(notificationData, walletId);
if (root.shouldSkip(notificationData, last))
return;
var config = configService.getSync();
config.colorFor = config.colorFor || {};
var color = config.colorFor[walletId] || '#1ABC9C';
switch (notificationData.type) {
case 'NewTxProposal':
notification.new('New Transaction',
walletName, {color: color} );
break;
case 'TxProposalAcceptedBy':
notification.success('Transaction Signed',
walletName, {color: color} );
break;
case 'TxProposalRejectedBy':
notification.error('Transaction Rejected',
walletName, {color: color} );
break;
case 'TxProposalFinallyRejected':
notification.error('A transaction was finally rejected',
walletName, {color: color} );
break;
case 'NewOutgoingTx':
notification.sent('Transaction Sent',
walletName, {color: color} );
break;
case 'NewIncomingTx':
notification.funds('Funds received',
walletName, {color: color} );
break;
case 'ScanFinished':
notification.success('Scan Finished',
walletName, {color: color} );;
break;
case 'NewCopayer':
// No UX notification
break;
}
};
return root;
});

View file

@ -0,0 +1,10 @@
'use strict';
angular.module('copayApp.services').factory('pluginManager', function() {
var root = {};
root.getInstance = function(config){
return new copay.PluginManager(config);
};
return root;
});

View file

@ -0,0 +1,359 @@
'use strict';
angular.module('copayApp.services')
.factory('profileService', function profileServiceFactory($rootScope, $location, $timeout, $filter, $log, lodash, pluginManager, balanceService, applicationService, storageService, bwcService, configService, notificationService, notification) {
var root = {};
root.profile = null;
root.focusedClient = null;
root.walletClients = {};
root.getUtils = function() {
return bwcService.getUtils();
};
root.formatAmount = function(amount) {
var config = configService.getSync().wallet.settings;
if (config.unitCode == 'sat') return amount;
//TODO : now only works for english, specify opts to change thousand separator and decimal separator
return this.getUtils().formatAmount(amount, config.unitCode);
};
root._setFocus = function(walletId, cb) {
$log.debug('Set focus:', walletId);
// Set local object
root.focusedClient = root.walletClients[walletId];
if (lodash.isEmpty(root.focusedClient)) {
root.focusedClient = root.walletClients[lodash.keys(root.walletClients)[0]];
}
if (lodash.isEmpty(root.focusedClient)) {
$rootScope.$emit('Local/NoWallets');
}
// set if completed
if (!lodash.isEmpty(root.focusedClient)) {
$rootScope.$emit('Local/NewFocusedWallet');
}
return cb();
};
root.setAndStoreFocus = function(walletId, cb) {
root._setFocus(walletId, function() {
storageService.storeFocusedWalletId(walletId, cb);
});
};
root.setWalletClients = function() {
lodash.each(root.profile.credentials, function(credentials) {
if (root.walletClients[credentials.walletId] &&
root.walletClients[credentials.walletId].started) {
return;
}
var client = bwcService.getClient(JSON.stringify(credentials));
root.walletClients[credentials.walletId] = client;
client.removeAllListeners();
client.on('notification', function(n) {
$log.debug('BWC Notification:', n);
notificationService.newBWCNotification(n,
client.credentials.walletId, client.credentials.walletName);
// Actions for both focuses and unfocuses wallets...
if (n.type == 'ScanFinished') {
client.scanning = false;
}
if (root.focusedClient.credentials.walletId == client.credentials.walletId) {
$rootScope.$emit(n.type);
} else {
$rootScope.$apply();
}
});
client.on('walletCompleted', function() {
$log.debug('Wallet completed');
root.updateCredentialsFC(function() {
$rootScope.$emit('Local/WalletCompleted')
});
});
root.walletClients[credentials.walletId].started = true;
client.initNotifications(function(err) {
if (err) {
$log.error('Could not init notifications err:', err);
root.walletClients[credentials.walletId].started = false;
return;
}
});
});
$rootScope.$emit('Local/WalletListUpdated');
};
root.bindProfile = function(profile, cb) {
root.profile = profile;
configService.get(function(err) {
if (err) return cb(err);
root.setWalletClients();
storageService.getFocusedWalletId(function(err, focusedWalletId) {
if (err) return cb(err);
root._setFocus(focusedWalletId, cb);
});
});
};
root.loadAndBindProfile = function(cb) {
storageService.getProfile(function(err, profile) {
if (err) {
notification.error('CRITICAL ERROR: ' + err);
return cb(err);
}
if (!profile) return cb(new Error('NOPROFILE: No profile'));
return root.bindProfile(profile, cb);
});
};
root._createNewProfile = function(pin, cb) {
var walletClient = bwcService.getClient();
walletClient.createWallet('Personal Wallet', 'me', 1, 1, {
network: 'livenet'
}, function(err) {
if (err) return cb('Error creating wallet. Check your internet connection');
var p = Profile.create({
credentials: [JSON.parse(walletClient.export())],
});
return cb(null, p);
})
};
// TODO copayer name
root.createWallet = function(opts, cb) {
var walletClient = bwcService.getClient();
$log.debug('Creating Wallet:', opts);
if (opts.extendedPrivateKey) {
try {
walletClient.seedFromExtendedPrivateKey(opts.extendedPrivateKey);
} catch (ex) {
return cb('Could not create using the specified extended private key');
}
}
walletClient.createWallet(opts.name, opts.myName || 'me', opts.m, opts.n, {
network: opts.networkName
}, function(err, secret) {
if (err) return cb('Error creating wallet');
root.profile.credentials.push(JSON.parse(walletClient.export()));
root.setWalletClients();
root.setAndStoreFocus(walletClient.credentials.walletId, function() {
storageService.storeProfile(root.profile, function(err) {
return cb(null, secret);
});
});
})
};
root.joinWallet = function(opts, cb) {
var walletClient = bwcService.getClient();
$log.debug('Joining Wallet:', opts);
if (opts.extendedPrivateKey) {
try {
walletClient.seedFromExtendedPrivateKey(opts.extendedPrivateKey);
} catch (ex) {
return cb('Could not join using the specified extended private key');
}
}
// TODO name
walletClient.joinWallet(opts.secret, opts.myName || 'me', function(err) {
// TODO: err
if (err) return cb('Error joining wallet' + err);
root.profile.credentials.push(JSON.parse(walletClient.export()));
root.setWalletClients();
root.setAndStoreFocus(walletClient.credentials.walletId, function() {
storageService.storeProfile(root.profile, function(err) {
return cb(null, secret);
});
});
})
};
root.deleteWalletFC = function(opts, cb) {
var fc = root.focusedClient;
$log.debug('Deleting Wallet:', fc.credentials.walletName);
fc.removeAllListeners();
root.profile.credentials = lodash.reject(root.profile.credentials, {
walletId: fc.credentials.walletId
});
delete root.walletClients[fc.credentials.walletId];
root.focusedClient = null;
$timeout(function() {
root.setWalletClients();
root.setAndStoreFocus(null, function() {
storageService.storeProfile(root.profile, function(err) {
if (err) return cb(err);
return cb();
});
});
});
};
root.importWallet = function(str, opts, cb) {
var walletClient = bwcService.getClient();
$log.debug('Importing Wallet:', opts);
try {
walletClient.import(str, {
compressed: opts.compressed,
password: opts.password
});
} catch (err) {
return cb('Could not import. Check input file and password');
}
var walletId = walletClient.credentials.walletId;
// check if exist
if (lodash.find(root.profile.credentials, {
'walletId': walletId
})) {
return cb('Wallet already exists');
}
root.profile.credentials.push(JSON.parse(walletClient.export()));
root.setWalletClients();
root.setAndStoreFocus(walletId, function() {
storageService.storeProfile(root.profile, function(err) {
$rootScope.$emit('Local/WalletImported', walletId);
return cb(null, walletId);
});
});
};
root.create = function(pin, cb) {
root._createNewProfile(pin, function(err, p) {
if (err) return cb(err);
root.bindProfile(p, function(err) {
storageService.storeNewProfile(p, function(err) {
return cb(err);
});
});
});
};
root.importLegacyWallet = function(username, password, blob, cb) {
var walletClient = bwcService.getClient();
walletClient.createWalletFromOldCopay(username, password, blob, function(err, existed) {
if (err) return cb('Error importing wallet: ' + err);
if (root.walletClients[walletClient.credentials.walletId]) {
$log.debug('Wallet:' + walletClient.credentials.walletName + ' already imported');
return cb('Wallet Already Imported: ' + walletClient.credentials.walletName);
};
$log.debug('Creating Wallet:', walletClient.credentials.walletName);
root.profile.credentials.push(JSON.parse(walletClient.export()));
root.setWalletClients();
root.setAndStoreFocus(walletClient.credentials.walletId, function() {
storageService.storeProfile(root.profile, function(err) {
return cb(null, walletClient.credentials.walletId, walletClient.credentials.walletName, existed);
});
});
});
};
root.updateCredentialsFC = function(cb) {
var fc = root.focusedClient;
var newCredentials = lodash.reject(root.profile.credentials, {
walletId: fc.credentials.walletId
});
newCredentials.push(JSON.parse(fc.export()));
root.profile.credentials = newCredentials;
storageService.storeProfile(root.profile, cb);
};
root.setPrivateKeyEncryptionFC = function(password, cb) {
var fc = root.focusedClient;
$log.debug('Encrypting private key for', fc.credentials.walletName);
fc.setPrivateKeyEncryption(password);
fc.lock();
root.updateCredentialsFC(function() {
$log.debug('Wallet encrypted');
return cb();
});
};
root.disablePrivateKeyEncryptionFC = function(cb) {
var fc = root.focusedClient;
$log.debug('Disabling private key encryption for', fc.credentials.walletName);
try {
fc.disablePrivateKeyEncryption();
} catch (e) {
return cb(e);
}
root.updateCredentialsFC(function() {
$log.debug('Wallet encryption disabled');
return cb();
});
};
root.lockFC = function() {
var fc = root.focusedClient;
try {
fc.lock();
} catch (e) {};
};
root.unlockFC = function(cb) {
var fc = root.focusedClient;
$log.debug('Wallet is encrypted');
$rootScope.$emit('Local/NeedsPassword', false, function(err2, password) {
if (err2 || !password) {
return cb(err2 || 'Password needed');
}
try {
fc.unlock(password);
} catch (e) {
$log.debug(e);
return cb('Wrong password');
}
$timeout(function() {
if( fc.isPrivKeyEncrypted()) {
$log.debug('Locking wallet automatically');
root.lockFC();
};
},2000);
return cb();
});
};
return root;
});

View file

@ -0,0 +1,182 @@
'use strict';
//var util = require('util');
//var _ = require('lodash');
//var log = require('../util/log');
//var preconditions = require('preconditions').singleton();
//var request = require('request');
/*
This class lets interfaces with BitPay's exchange rate API.
*/
var RateService = function(opts) {
var self = this;
opts = opts || {};
self.httprequest = opts.httprequest; // || request;
self.lodash = opts.lodash;
self.SAT_TO_BTC = 1 / 1e8;
self.BTC_TO_SAT = 1e8;
self.UNAVAILABLE_ERROR = 'Service is not available - check for service.isAvailable() or use service.whenAvailable()';
self.UNSUPPORTED_CURRENCY_ERROR = 'Currency not supported';
self._url = opts.url || 'https://insight.bitpay.com:443/api/rates';
self._isAvailable = false;
self._rates = {};
self._alternatives = [];
self._queued = [];
self._fetchCurrencies();
};
var _instance;
RateService.singleton = function(opts) {
if (!_instance) {
_instance = new RateService(opts);
}
return _instance;
};
RateService.prototype._fetchCurrencies = function() {
var self = this;
var backoffSeconds = 5;
var updateFrequencySeconds = 3600;
var rateServiceUrl = 'https://bitpay.com/api/rates';
var retrieve = function() {
//log.info('Fetching exchange rates');
self.httprequest.get(rateServiceUrl).success(function(res) {
self.lodash.each(res, function(currency) {
self._rates[currency.code] = currency.rate;
self._alternatives.push({
name: currency.name,
isoCode: currency.code,
rate: currency.rate
});
});
self._isAvailable = true;
self.lodash.each(self._queued, function(callback) {
setTimeout(callback, 1);
});
setTimeout(retrieve, updateFrequencySeconds * 1000);
}).error(function(err) {
//log.debug('Error fetching exchange rates', err);
setTimeout(function() {
backoffSeconds *= 1.5;
retrieve();
}, backoffSeconds * 1000);
return;
});
};
retrieve();
};
RateService.prototype.getRate = function(code) {
return this._rates[code];
};
RateService.prototype.getHistoricRate = function(code, date, cb) {
var self = this;
self.httprequest.get(self._url + '/' + code + '?ts=' + date)
.success(function(body) {
return cb(null, body.rate)
})
.error(function(err) {
return cb(err)
});
};
RateService.prototype.getHistoricRates = function(code, dates, cb) {
var self = this;
var tsList = dates.join(',');
self.httprequest.get(self._url + '/' + code + '?ts=' + tsList)
.success(function(body) {
if (!self.lodash.isArray(body)) {
body = [{
ts: dates[0],
rate: body.rate
}];
}
return cb(null, body);
})
.error(function(err) {
return cb(err)
});
};
RateService.prototype.getAlternatives = function() {
return this._alternatives;
};
RateService.prototype.isAvailable = function() {
return this._isAvailable;
};
RateService.prototype.whenAvailable = function(callback) {
if (this.isAvailable()) {
setTimeout(callback, 1);
} else {
this._queued.push(callback);
}
};
RateService.prototype.toFiat = function(satoshis, code) {
if (!this.isAvailable()) {
return null;
}
return satoshis * this.SAT_TO_BTC * this.getRate(code);
};
RateService.prototype.toFiatHistoric = function(satoshis, code, date, cb) {
var self = this;
self.getHistoricRate(code, date, function(err, rate) {
if (err) return cb(err);
return cb(null, satoshis * self.SAT_TO_BTC * rate);
});
};
RateService.prototype.fromFiat = function(amount, code) {
if (!this.isAvailable()) {
return null;
}
return amount / this.getRate(code) * this.BTC_TO_SAT;
};
RateService.prototype.listAlternatives = function() {
var self = this;
if (!this.isAvailable()) {
return [];
}
return self.lodash.map(this.getAlternatives(), function(item) {
return {
name: item.name,
isoCode: item.isoCode
}
});
};
angular.module('copayApp.services').factory('rateService', function($http, lodash) {
// var cfg = _.extend(config.rates, {
// httprequest: $http
// });
var cfg = {
httprequest: $http,
lodash: lodash
};
return RateService.singleton(cfg);
});

7
src/js/services/sjcl.js Normal file
View file

@ -0,0 +1,7 @@
'use strict';
angular.module('copayApp.services')
.factory('sjcl', function bitcoreFactory(bwcService) {
var sjcl = bwcService.getSJCL();
return sjcl;
});

View file

@ -0,0 +1,111 @@
'use strict';
angular.module('copayApp.services')
.factory('storageService', function(localStorageService, sjcl, $log, lodash) {
var root = {};
var getUUID = function(cb) {
// TO SIMULATE MOBILE
//return cb('hola');
if (!window || !window.plugins || !window.plugins.uniqueDeviceID)
return cb(null);
window.plugins.uniqueDeviceID.get(
function(uuid) {
return cb(uuid);
}, cb);
};
var encryptOnMobile = function(text, cb) {
getUUID(function(uuid) {
if (uuid) {
$log.debug('Encrypting profile');
text = sjcl.encrypt(uuid, text);
}
return cb(null, text);
});
};
var decryptOnMobile = function(text, cb) {
var json;
try {
json = JSON.parse(text);
} catch (e) {};
if (!json.iter || !json.ct)
return cb(null, text);
$log.debug('Profile is encrypted');
getUUID(function(uuid) {
if (!uuid)
return cb(new Error('Could not decrypt localstorage profile'));
text = sjcl.decrypt(uuid, text);
return cb(null, text);
});
};
root.storeNewProfile = function(profile, cb) {
encryptOnMobile(profile.toObj(), function(err, x) {
localStorageService.create('profile', x, cb);
});
};
root.storeProfile = function(profile, cb) {
encryptOnMobile(profile.toObj(), function(err, x) {
localStorageService.set('profile', x, cb);
});
};
root.getProfile = function(cb) {
localStorageService.get('profile', function(err, str) {
if (err || !str) return cb(err);
decryptOnMobile(str, function(err, str) {
if (err) return cb(err);
var p, err;
try {
p = Profile.fromString(str);
} catch (e) {
err = new Error('Could not read profile:' + p);
}
return cb(err, p);
});
});
};
root.deleteProfile = function(cb) {
localStorageService.remove('profile', cb);
};
root.storeFocusedWalletId = function(id, cb) {
localStorageService.set('focusedWalletId', id, cb);
};
root.getFocusedWalletId = function(cb) {
localStorageService.get('focusedWalletId', cb);
};
root.getLastAddress = function(walletId, cb) {
localStorageService.get('lastAddress-' + walletId, cb);
};
root.storeLastAddress = function(walletId, address, cb) {
localStorageService.set('lastAddress-' + walletId, address, cb);
};
root.clearLastAddress = function(walletId, cb) {
localStorageService.remove('lastAddress-' + walletId, cb);
};
root.setBackupFlag = function(walletId, cb) {
localStorageService.set('backup-' + walletId, Date.now(), cb);
};
root.getBackupFlag = function(walletId, cb) {
localStorageService.get('backup-' + walletId, cb);
};
return root;
});

View file

@ -0,0 +1,47 @@
'use strict';
angular.module('copayApp.services').factory('txStatus', function($modal, lodash, profileService) {
var root = {};
root.notify = function(txp) {
var fc = profileService.focusedClient;
var msg;
var status = txp.status;
if (status == 'broadcasted') {
msg = 'Transaction broadcasted';
}
else {
var action = lodash.find(txp.actions, {
copayerId: fc.credentials.copayerId
});
if (!action) {
msg = 'Transaction proposal created';
} else if (action.type == 'accept') {
msg = 'Transaction proposal signed';
} else if (action.type == 'reject') {
msg = 'Transaction was rejected';
}
}
if (msg)
root.openModal(msg);
};
root.openModal = function(statusStr) {
var ModalInstanceCtrl = function($scope, $modalInstance) {
$scope.statusStr = statusStr;
$scope.cancel = function() {
$modalInstance.dismiss('cancel');
};
};
$modal.open({
templateUrl: 'views/modals/tx-status.html',
windowClass: 'full',
controller: ModalInstanceCtrl,
});
};
return root;
});

View file

@ -0,0 +1,14 @@
'use strict';
var UriHandler = function() {};
UriHandler.prototype.register = function() {
var base = window.location.origin + '/';
var url = base + '#/uri-payment/%s';
if(navigator.registerProtocolHandler) {
navigator.registerProtocolHandler('bitcoin', url, 'Copay');
}
};
angular.module('copayApp.services').value('uriHandler', new UriHandler());