bwc
This commit is contained in:
parent
04fb7ba032
commit
320de62f13
348 changed files with 7745 additions and 30874 deletions
24
src/js/services/applicationService.js
Normal file
24
src/js/services/applicationService.js
Normal 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;
|
||||
});
|
||||
83
src/js/services/backupService.js
Normal file
83
src/js/services/backupService.js
Normal 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;
|
||||
});
|
||||
92
src/js/services/balanceService.js
Normal file
92
src/js/services/balanceService.js
Normal 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;
|
||||
});
|
||||
6
src/js/services/bitcore.js
Normal file
6
src/js/services/bitcore.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
'use strict';
|
||||
angular.module('copayApp.services')
|
||||
.factory('bitcore', function bitcoreFactory(bwcService) {
|
||||
var bitcore = bwcService.getBitcore();
|
||||
return bitcore;
|
||||
});
|
||||
125
src/js/services/configService.js
Normal file
125
src/js/services/configService.js
Normal 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
95
src/js/services/go.js
Normal 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;
|
||||
});
|
||||
3
src/js/services/isCordova.js
Normal file
3
src/js/services/isCordova.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('copayApp.services').value('isCordova', window.cordova ? true : false);
|
||||
29
src/js/services/isMobile.js
Normal file
29
src/js/services/isMobile.js
Normal 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);
|
||||
139
src/js/services/legacyImportService.js
Normal file
139
src/js/services/legacyImportService.js
Normal 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;
|
||||
});
|
||||
92
src/js/services/localStorage.js
Normal file
92
src/js/services/localStorage.js
Normal 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;
|
||||
});
|
||||
295
src/js/services/notifications.js
Normal file
295
src/js/services/notifications.js
Normal 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);
|
||||
};
|
||||
}
|
||||
]
|
||||
|
||||
};
|
||||
});
|
||||
94
src/js/services/notificationsService.js
Normal file
94
src/js/services/notificationsService.js
Normal 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;
|
||||
});
|
||||
10
src/js/services/pluginManager.js
Normal file
10
src/js/services/pluginManager.js
Normal 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;
|
||||
});
|
||||
359
src/js/services/profileService.js
Normal file
359
src/js/services/profileService.js
Normal 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;
|
||||
});
|
||||
182
src/js/services/rateService.js
Normal file
182
src/js/services/rateService.js
Normal 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
7
src/js/services/sjcl.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
|
||||
'use strict';
|
||||
angular.module('copayApp.services')
|
||||
.factory('sjcl', function bitcoreFactory(bwcService) {
|
||||
var sjcl = bwcService.getSJCL();
|
||||
return sjcl;
|
||||
});
|
||||
111
src/js/services/storageService.js
Normal file
111
src/js/services/storageService.js
Normal 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;
|
||||
});
|
||||
47
src/js/services/txStatus.js
Normal file
47
src/js/services/txStatus.js
Normal 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;
|
||||
});
|
||||
14
src/js/services/uriHandler.js
Normal file
14
src/js/services/uriHandler.js
Normal 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());
|
||||
Loading…
Add table
Add a link
Reference in a new issue