Wallet/js/controllers/send.js

569 lines
17 KiB
JavaScript
Raw Normal View History

2014-03-26 09:18:42 -03:00
'use strict';
2014-06-12 17:42:26 -03:00
var bitcore = require('bitcore');
2014-09-05 16:16:06 -07:00
var preconditions = require('preconditions').singleton();
2014-03-26 09:18:42 -03:00
2014-06-03 17:42:36 -03:00
angular.module('copayApp.controllers').controller('SendController',
2014-11-20 01:48:22 -03:00
function($scope, $rootScope, $window, $timeout, $modal, isMobile, notification, controllerUtils, rateService) {
controllerUtils.redirIfNotComplete();
var w = $rootScope.wallet;
preconditions.checkState(w);
preconditions.checkState(w.settings.unitToSatoshi);
$rootScope.title = 'Send';
$scope.loading = false;
2014-11-20 03:10:43 -03:00
$scope.error = $scope.success = null;
var satToUnit = 1 / w.settings.unitToSatoshi;
$scope.defaultFee = bitcore.TransactionBuilder.FEE_PER_1000B_SAT * satToUnit;
$scope.unitToBtc = w.settings.unitToSatoshi / bitcore.util.COIN;
$scope.unitToSatoshi = w.settings.unitToSatoshi;
$scope.alternativeName = w.settings.alternativeName;
$scope.alternativeIsoCode = w.settings.alternativeIsoCode;
$scope.isRateAvailable = false;
$scope.rateService = rateService;
2014-11-23 15:52:39 -03:00
$scope.showScanner = false;
2014-11-26 08:38:02 -03:00
$scope.myId = w.getMyCopayerId();
$scope.isMobile = isMobile.any();
rateService.whenAvailable(function() {
$scope.isRateAvailable = true;
$scope.$digest();
});
2014-08-28 11:37:39 -03:00
/**
* Setting the two related amounts as properties prevents an infinite
* recursion for watches while preserving the original angular updates
*/
2014-08-27 14:20:13 -03:00
Object.defineProperty($scope,
"alternative", {
2014-09-10 15:09:11 -03:00
get: function() {
return this._alternative;
},
set: function(newValue) {
this._alternative = newValue;
if (typeof(newValue) === 'number' && $scope.isRateAvailable) {
this._amount = parseFloat(
2014-09-05 16:16:06 -07:00
(rateService.fromFiat(newValue, w.settings.alternativeIsoCode) * satToUnit).toFixed(w.settings.unitDecimals), 10);
2014-09-10 15:09:11 -03:00
} else {
this._amount = 0;
}
},
enumerable: true,
configurable: true
});
2014-08-27 14:20:13 -03:00
Object.defineProperty($scope,
"amount", {
2014-09-10 15:09:11 -03:00
get: function() {
return this._amount;
},
set: function(newValue) {
this._amount = newValue;
if (typeof(newValue) === 'number' && $scope.isRateAvailable) {
2014-10-29 18:34:59 -03:00
2014-09-10 15:09:11 -03:00
this._alternative = parseFloat(
2014-09-05 16:16:06 -07:00
(rateService.toFiat(newValue * w.settings.unitToSatoshi, w.settings.alternativeIsoCode)).toFixed(2), 10);
2014-09-10 15:09:11 -03:00
} else {
this._alternative = 0;
}
},
enumerable: true,
configurable: true
});
2014-11-12 18:32:18 -03:00
$scope.loadTxs = function() {
controllerUtils.updateTxs();
setTimeout(function() {
$scope.loading = false;
$rootScope.$digest();
}, 1);
}
2014-06-18 20:18:13 -03:00
$scope.showAddressBook = function() {
2014-10-23 11:38:56 -03:00
return w && _.keys(w.addressBook).length > 0;
2014-06-18 20:18:13 -03:00
};
if ($rootScope.pendingPayment) {
var pp = $rootScope.pendingPayment;
2014-08-15 23:00:12 -03:00
$scope.address = pp.address + '';
2014-09-05 16:16:06 -07:00
var amount = pp.data.amount / w.settings.unitToSatoshi * 100000000;
$scope.amount = amount;
$scope.commentText = pp.data.message;
}
2014-05-06 17:02:49 -03:00
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
window.URL = window.URL || window.webkitURL || window.mozURL || window.msURL;
if (!window.cordova && !navigator.getUserMedia)
2014-09-10 15:09:11 -03:00
$scope.disableScanner = 1;
2014-09-08 17:07:43 -03:00
$scope._showError = function(err) {
copay.logger.error(err);
var msg = err.toString();
if (msg.match('BIG'))
2014-11-23 15:52:39 -03:00
msg = 'The transaction have too many inputs. Try creating many transactions for smaller amounts'
if (msg.match('totalNeededAmount'))
2014-11-23 15:52:39 -03:00
msg = 'Not enough funds'
var message = 'The transaction' + (w.isShared() ? ' proposal' : '') +
' could not be created: ' + msg;
$scope.error = message;
$scope.loading = false;
$scope.loadTxs();
};
$scope.submitForm = function(form) {
if (form.$invalid) {
2014-11-03 16:06:17 -03:00
$scope.error = 'Unable to send transaction proposal';
return;
}
2014-04-24 22:43:19 -03:00
$scope.loading = true;
var address = form.address.$modelValue;
2014-09-05 16:16:06 -07:00
var amount = parseInt((form.amount.$modelValue * w.settings.unitToSatoshi).toFixed(0));
2014-06-13 19:45:00 -03:00
var commentText = form.comment.$modelValue;
var payInfo;
if (address.indexOf('bitcoin:') === 0) {
payInfo = (new bitcore.BIP21(address)).data;
} else if (/^https?:\/\//.test(address)) {
payInfo = {
merchant: address
};
}
// If we're setting the domain, ignore the change.
2014-09-10 15:09:11 -03:00
if ($rootScope.merchant && $rootScope.merchant.domain && address === $rootScope.merchant.domain) {
payInfo = {
merchant: $rootScope.merchant.request_url
};
}
2014-11-23 15:52:39 -03:00
w.spend({
toAddress: address,
amountSat: amount,
comment: commentText,
2014-11-23 00:29:50 -03:00
url: (payInfo && payInfo.merchant) ? payInfo.merchant : null,
2014-11-23 15:52:39 -03:00
}, function(err, txid, status) {
// reset fields
$scope.address = $scope.amount = $scope.commentText = null;
form.address.$pristine = form.amount.$pristine = true;
$rootScope.pendingPayment = null;
if (err) return $scope._showError(err);
$scope.notifyStatus(status);
$scope.loadTxs();
});
2014-05-06 17:02:49 -03:00
};
// QR code Scanner
var cameraInput;
var video;
var canvas;
var $video;
var context;
var localMediaStream;
var _scan = function(evt) {
if ($scope.isMobile) {
$scope.scannerLoading = true;
var files = evt.target.files;
if (files.length === 1 && files[0].type.indexOf('image/') === 0) {
var file = files[0];
var reader = new FileReader();
reader.onload = (function(theFile) {
return function(e) {
var mpImg = new MegaPixImage(file);
2014-06-12 17:42:26 -03:00
mpImg.render(canvas, {
maxWidth: 200,
maxHeight: 200,
orientation: 6
});
2014-05-06 17:02:49 -03:00
$timeout(function() {
qrcode.width = canvas.width;
qrcode.height = canvas.height;
qrcode.imagedata = context.getImageData(0, 0, qrcode.width, qrcode.height);
try {
qrcode.decode();
} catch (e) {
2014-06-03 18:38:56 -03:00
// error decoding QR
2014-05-06 17:02:49 -03:00
}
}, 1500);
};
})(file);
// Read in the file as a data URL
reader.readAsDataURL(file);
}
} else {
if (localMediaStream) {
context.drawImage(video, 0, 0, 300, 225);
try {
qrcode.decode();
2014-06-12 17:42:26 -03:00
} catch (e) {
2014-05-06 17:02:49 -03:00
//qrcodeError(e);
}
}
$timeout(_scan, 500);
}
};
var _successCallback = function(stream) {
video.src = (window.URL && window.URL.createObjectURL(stream)) || stream;
localMediaStream = stream;
video.play();
$timeout(_scan, 1000);
};
var _scanStop = function() {
$scope.scannerLoading = false;
$scope.showScanner = false;
if (!$scope.isMobile) {
if (localMediaStream && localMediaStream.stop) localMediaStream.stop();
localMediaStream = null;
video.src = '';
}
};
var _videoError = function(err) {
_scanStop();
};
qrcode.callback = function(data) {
_scanStop();
2014-05-07 11:21:30 -03:00
$scope.$apply(function() {
$scope.sendForm.address.$setViewValue(data);
2014-11-03 15:13:53 -03:00
$scope.sendForm.address.$render();
2014-05-07 11:21:30 -03:00
});
2014-05-06 17:02:49 -03:00
};
$scope.cancelScanner = function() {
_scanStop();
};
$scope.openScanner = function() {
if (window.cordova) return $scope.scannerIntent();
2014-05-06 17:02:49 -03:00
$scope.showScanner = true;
// Wait a moment until the canvas shows
$timeout(function() {
canvas = document.getElementById('qr-canvas');
context = canvas.getContext('2d');
if ($scope.isMobile) {
cameraInput = document.getElementById('qrcode-camera');
cameraInput.addEventListener('change', _scan, false);
} else {
video = document.getElementById('qrcode-scanner-video');
$video = angular.element(video);
canvas.width = 300;
canvas.height = 225;
context.clearRect(0, 0, 300, 225);
2014-04-24 22:43:19 -03:00
2014-06-12 17:42:26 -03:00
navigator.getUserMedia({
video: true
}, _successCallback, _videoError);
2014-05-06 17:02:49 -03:00
}
}, 500);
2014-04-24 22:43:19 -03:00
};
2014-06-18 01:00:32 -03:00
2014-07-25 11:08:29 -03:00
$scope.scannerIntent = function() {
cordova.plugins.barcodeScanner.scan(
function onSuccess(result) {
if (result.cancelled) return;
$timeout(function() {
var data = result.text;
$scope.$apply(function() {
$scope.sendForm.address.$setViewValue(result.text);
$scope.sendForm.address.$render();
});
}, 1000);
2014-07-25 11:08:29 -03:00
},
function onError(error) {
alert('Scanning error');
});
}
$scope.toggleAddressBookEntry = function(key) {
w.toggleAddressBookEntry(key);
2014-06-18 01:00:32 -03:00
};
$scope.copyAddress = function(address) {
$scope.address = address;
};
$scope.openAddressBookModal = function() {
var modalInstance = $modal.open({
templateUrl: 'views/modals/address-book.html',
2014-06-18 01:00:32 -03:00
windowClass: 'tiny',
controller: function($scope, $modalInstance) {
2014-06-18 01:00:32 -03:00
$scope.submitAddressBook = function(form) {
if (form.$invalid) {
return;
}
var entry = {
"address": form.newaddress.$modelValue,
"label": form.newlabel.$modelValue
};
form.newaddress.$pristine = form.newlabel.$pristine = true;
$modalInstance.close(entry);
};
$scope.cancel = function() {
$modalInstance.dismiss('cancel');
};
},
});
modalInstance.result.then(function(entry) {
$timeout(function() {
$scope.loading = false;
var errorMsg;
try {
2014-06-18 20:18:13 -03:00
w.setAddressBook(entry.address, entry.label);
2014-06-18 01:00:32 -03:00
} catch (e) {
errorMsg = e.message;
}
2014-11-20 02:05:57 -03:00
// TODO change this notifications
2014-06-26 17:17:24 -03:00
if (errorMsg) {
notification.error('Error', errorMsg);
} else {
notification.success('Success', 'New entry has been created');
}
2014-06-18 01:00:32 -03:00
$rootScope.$digest();
}, 500);
// reset fields
$scope.newaddress = $scope.newlabel = null;
});
};
2014-11-12 18:32:18 -03:00
$scope.setTopAmount = function() {
$scope.amount = $rootScope.topAmount;
2014-06-19 15:55:04 -03:00
};
2014-11-23 15:52:39 -03:00
$scope.notifyStatus = function(status) {
if (status == copay.Wallet.TX_BROADCASTED)
notification.success('Success', 'Transaction broadcasted!');
else if (status == copay.Wallet.TX_PROPOSAL_SENT)
notification.success('Success', 'Transaction proposal created');
else if (status == copay.Wallet.TX_SIGNED)
notification.success('Success', 'Transaction proposal was signed');
2014-11-27 17:45:10 -03:00
else if (status == copay.Wallet.TX_SIGNED_AND_BROADCASTED)
notification.success('Success', 'Transaction signed and broadcasted!');
2014-11-23 15:52:39 -03:00
else
notification.error('Error', 'Unknown error occured');
};
$scope.send = function(ntxid, cb) {
2014-11-20 03:10:43 -03:00
$scope.error = $scope.success = null;
$scope.loading = true;
$rootScope.txAlertCount = 0;
2014-11-27 17:45:10 -03:00
w.issueTx(ntxid, function(err, txid, status) {
2014-11-23 15:52:39 -03:00
$scope.notifyStatus(status);
if (cb) return cb();
else $scope.loadTxs();
});
};
$scope.sign = function(ntxid) {
$scope.loading = true;
2014-11-20 03:10:43 -03:00
$scope.error = $scope.success = null;
2014-11-06 14:53:09 -03:00
2014-11-23 15:52:39 -03:00
w.signAndSend(ntxid, function(err, id, status) {
$scope.notifyStatus(status);
2014-11-06 14:53:09 -03:00
$scope.loadTxs();
2014-11-23 15:52:39 -03:00
});
};
$scope.reject = function(ntxid) {
$scope.loading = true;
$rootScope.txAlertCount = 0;
w.reject(ntxid);
notification.warning('Transaction rejected', 'You rejected the transaction successfully');
$scope.loadTxs();
};
$scope.clearMerchant = function(callback) {
// TODO: Find a better way of detecting
// whether we're in the Send scope or not.
2014-11-20 03:10:43 -03:00
if (!$scope.sendForm || !$scope.sendForm.address) {
delete $rootScope.merchant;
2014-10-31 16:10:11 -03:00
$rootScope.merchantError = false;
if (callback) callback();
return;
}
2014-11-20 03:10:43 -03:00
var val = $scope.sendForm.address.$viewValue || '';
var uri;
// If we're setting the domain, ignore the change.
if ($rootScope.merchant && $rootScope.merchant.domain && val === $rootScope.merchant.domain) {
uri = {
merchant: $rootScope.merchant.request_url
};
}
if (val.indexOf('bitcoin:') === 0) {
uri = new bitcore.BIP21(val).data;
} else if (/^https?:\/\//.test(val)) {
uri = {
merchant: val
};
}
if (!uri || !uri.merchant) {
delete $rootScope.merchant;
2014-11-20 03:10:43 -03:00
$scope.sendForm.amount.$setViewValue('');
$scope.sendForm.amount.$render();
if (callback) callback();
if ($rootScope.$$phase !== '$apply' && $rootScope.$$phase !== '$digest') {
$rootScope.$apply();
}
}
};
$scope.cancelSend = function(form) {
delete $rootScope.merchant;
$rootScope.merchantError = false;
form.address.$setViewValue('');
form.address.$render();
form.amount.$setViewValue('');
form.comment.$setViewValue('');
form.$setPristine();
};
$scope.onChanged = function() {
2014-11-20 03:10:43 -03:00
var value = $scope.address || '';
2014-08-13 18:54:07 -04:00
var uri;
2014-11-20 03:10:43 -03:00
$scope.error = $scope.success = null;
// If we're setting the domain, ignore the change.
2014-09-10 15:09:11 -03:00
if ($rootScope.merchant && $rootScope.merchant.domain && value === $rootScope.merchant.domain) {
return;
}
2014-08-19 10:21:40 -07:00
if (value.indexOf('bitcoin:') === 0) {
uri = new bitcore.BIP21(value).data;
} else if (/^https?:\/\//.test(value)) {
2014-08-13 18:54:07 -04:00
uri = {
merchant: value
};
}
2014-08-19 10:21:40 -07:00
2014-08-13 18:54:07 -04:00
if (!uri || !uri.merchant) {
return;
}
2014-08-19 10:21:40 -07:00
var apply = function() {
if ($rootScope.$$phase !== '$apply' && $rootScope.$$phase !== '$digest') {
$rootScope.$apply();
}
};
2014-11-20 03:10:43 -03:00
$scope.fetchingURL = uri.merchant;
$scope.loading = true;
apply();
var timeout = setTimeout(function() {
timeout = null;
2014-11-20 03:10:43 -03:00
$scope.fetchingURL = null;
$scope.loading = false;
$scope.sendForm.address.$setViewValue('');
$scope.sendForm.address.$render();
$scope.sendForm.address.$isValid = false;
$scope.error = 'Payment server timed out';
apply();
}, 10 * 1000);
// Payment Protocol URI (BIP-72)
2014-11-23 15:52:39 -03:00
$scope.wallet.fetchPaymentRequest({
url: uri.merchant
}, function(err, merchantData) {
if (!timeout) return;
clearTimeout(timeout);
2014-11-20 03:10:43 -03:00
$scope.loading = false;
$scope.fetchingURL = null;
apply();
var balance = $rootScope.availableBalance;
2014-09-05 16:16:06 -07:00
var available = +(balance * w.settings.unitToSatoshi).toFixed(0);
if (merchantData && available < +merchantData.total) {
2014-10-31 16:10:11 -03:00
err = new Error('Insufficient funds.');
err.amount = merchantData.total;
}
if (err) {
if (err.amount) {
2014-11-20 03:10:43 -03:00
$scope.sendForm.amount.$setViewValue(+err.amount / w.settings.unitToSatoshi);
$scope.sendForm.amount.$render();
$scope.sendForm.amount.$isValid = false;
$scope.notEnoughAmount = true;
$rootScope.merchantError = true;
2014-11-20 03:10:43 -03:00
var lastAddr = $scope.sendForm.address.$viewValue;
var unregister = $scope.$watch('address', function() {
if ($scope.sendForm.address.$viewValue !== lastAddr) {
delete $rootScope.merchantError;
2014-11-20 03:10:43 -03:00
$scope.sendForm.amount.$setViewValue('');
$scope.sendForm.amount.$render();
unregister();
apply();
}
});
} else {
2014-11-20 03:10:43 -03:00
$scope.sendForm.address.$setViewValue('');
$scope.sendForm.address.$render();
}
2014-11-20 03:10:43 -03:00
$scope.sendForm.address.$isValid = false;
copay.logger.error(err);
2014-11-20 03:10:43 -03:00
$scope.error = 'Could not fetch payment request';
apply();
return;
}
var url = merchantData.request_url;
var domain = /^(?:https?)?:\/\/([^\/:]+).*$/.exec(url)[1];
2014-09-05 16:16:06 -07:00
merchantData.unitTotal = (+merchantData.total / w.settings.unitToSatoshi) + '';
merchantData.expiration = new Date(
2014-11-20 03:10:43 -03:00
merchantData.pr.pd.expires * 1000);
merchantData.domain = domain;
$rootScope.merchant = merchantData;
2014-11-20 03:10:43 -03:00
$scope.sendForm.address.$setViewValue(domain);
$scope.sendForm.address.$render();
$scope.sendForm.address.$isValid = true;
2014-11-20 03:10:43 -03:00
$scope.sendForm.amount.$setViewValue(merchantData.unitTotal);
$scope.sendForm.amount.$render();
$scope.sendForm.amount.$isValid = true;
// If the address changes to a non-payment-protocol one,
// delete the `merchant` property from the scope.
var unregister = $rootScope.$watch(function() {
$scope.clearMerchant(unregister);
});
apply();
});
};
2014-09-03 16:24:25 -03:00
});