From 021e9301d3d65ef8980f0b56ae5a54608b780eec Mon Sep 17 00:00:00 2001 From: Gustavo Maximiliano Cortez Date: Thu, 12 Jan 2017 20:29:34 -0300 Subject: [PATCH] Refactor selling bitcoin --- src/js/controllers/buyCoinbase.js | 7 +- src/js/controllers/sellCoinbase.js | 308 ++++++++++++++++++++++ src/js/services/coinbaseService.js | 3 +- src/sass/views/integrations/coinbase.scss | 24 +- www/views/buyCoinbase.html | 1 + www/views/coinbase.html | 2 +- www/views/sellCoinbase.html | 137 ++++++++++ 7 files changed, 459 insertions(+), 23 deletions(-) create mode 100644 src/js/controllers/sellCoinbase.js create mode 100644 www/views/sellCoinbase.html diff --git a/src/js/controllers/buyCoinbase.js b/src/js/controllers/buyCoinbase.js index b48af5961..c336ee75f 100644 --- a/src/js/controllers/buyCoinbase.js +++ b/src/js/controllers/buyCoinbase.js @@ -1,6 +1,6 @@ 'use strict'; -angular.module('copayApp.controllers').controller('buyCoinbaseController', function($scope, $log, $state, $timeout, $ionicHistory, lodash, coinbaseService, popupService, profileService, ongoingProcess, walletService) { +angular.module('copayApp.controllers').controller('buyCoinbaseController', function($scope, $log, $state, $timeout, $ionicHistory, $ionicScrollDelegate, lodash, coinbaseService, popupService, profileService, ongoingProcess, walletService) { var amount; var currency; @@ -13,6 +13,8 @@ angular.module('copayApp.controllers').controller('buyCoinbaseController', funct }; $scope.$on("$ionicView.beforeEnter", function(event, data) { + coinbaseService.setCredentials(); + amount = data.stateParams.amount; currency = data.stateParams.currency; @@ -140,6 +142,7 @@ angular.module('copayApp.controllers').controller('buyCoinbaseController', funct ongoingProcess.set('buyingBitcoin', false); $scope.buySuccess = updatedTx.data; $timeout(function() { + $ionicScrollDelegate.resize(); $scope.$apply(); }); }); @@ -152,7 +155,7 @@ angular.module('copayApp.controllers').controller('buyCoinbaseController', funct }; $scope.showWalletSelector = function() { - $scope.walletSelectorTitle = ($scope.action) == 'buy' ? 'Receive in' : 'Sell From'; + $scope.walletSelectorTitle = 'Receive in'; $scope.showWallets = true; }; diff --git a/src/js/controllers/sellCoinbase.js b/src/js/controllers/sellCoinbase.js new file mode 100644 index 000000000..58466ad7f --- /dev/null +++ b/src/js/controllers/sellCoinbase.js @@ -0,0 +1,308 @@ +'use strict'; + +angular.module('copayApp.controllers').controller('sellCoinbaseController', function($scope, $log, $state, $timeout, $ionicHistory, $ionicScrollDelegate, lodash, coinbaseService, popupService, profileService, ongoingProcess, walletService, appConfigService, configService) { + + var amount; + var currency; + + var showErrorAndBack = function(err) { + $scope.sendStatus = ''; + $log.error(err); + err = err.errors ? err.errors[0].message : err; + popupService.showAlert('Error', err, function() { + $ionicHistory.goBack(); + }); + }; + + var showError = function(err) { + $scope.sendStatus = ''; + $log.error(err); + err = err.errors ? err.errors[0].message : err; + popupService.showAlert('Error', err); + }; + + var publishAndSign = function (wallet, txp, onSendStatusChange, cb) { + if (!wallet.canSign() && !wallet.isPrivKeyExternal()) { + var err = 'No signing proposal: No private key'; + $log.info(err); + return cb(err); + } + + walletService.publishAndSign(wallet, txp, function(err, txp) { + if (err) return cb(err); + return cb(null, txp); + }, onSendStatusChange); + }; + + var checkTransaction = lodash.throttle(function(count, txp) { + $log.warn('Check if transaction has been received by Coinbase. Try ' + count + '/5'); + // TX amount in BTC + var satToBtc = 1 / 100000000; + var amountBTC = (txp.amount * satToBtc).toFixed(8); + coinbaseService.init(function(err, res) { + if (err) { + $log.error(err); + checkTransaction(count, txp); + return; + } + var accessToken = res.accessToken; + var accountId = res.accountId; + var sellPrice = null; + + coinbaseService.sellPrice(accessToken, currency, function(err, sell) { + if (err) { + $log.debug(err); + checkTransaction(count, txp); + return; + } + sellPrice = sell.data; + + coinbaseService.getTransactions(accessToken, accountId, function(err, ctxs) { + if (err) { + $log.debug(err); + checkTransaction(count, txp); + return; + } + + var coinbaseTransactions = ctxs.data; + var txFound = false; + var ctx; + for(var i = 0; i < coinbaseTransactions.length; i++) { + ctx = coinbaseTransactions[i]; + if (ctx.type == 'send' && ctx.from && ctx.amount.amount == amountBTC ) { + $log.warn('Transaction found!', ctx); + txFound = true; + $log.debug('Saving transaction to process later...'); + ctx['payment_method'] = $scope.selectedPaymentMethodId.value; + ctx['status'] = 'pending'; // Forcing "pending" status to process later + ctx['price_sensitivity'] = $scope.selectedPriceSensitivity.data; + ctx['sell_price_amount'] = sellPrice ? sellPrice.amount : ''; + ctx['sell_price_currency'] = sellPrice ? sellPrice.currency : 'USD'; + ctx['description'] = appConfigService.nameCase + ' Wallet: ' + $scope.wallet.name; + coinbaseService.savePendingTransaction(ctx, null, function(err) { + ongoingProcess.set('sellingBitcoin', false, statusChangeHandler); + if (err) $log.debug(err); + }); + return; + } + } + if (!txFound) { + // Transaction sent, but could not be verified by Coinbase.com + $log.warn('Transaction not found in Coinbase.'); + if (count < 5) { + checkTransaction(count + 1, txp); + } else { + ongoingProcess.set('sellingBitcoin', false, statusChangeHandler); + showError('No transaction found'); + return; + } + } + }); + }); + }); + }, 8000, { + 'leading': true + }); + + var statusChangeHandler = function (processName, showName, isOn) { + $log.debug('statusChangeHandler: ', processName, showName, isOn); + if ( processName == 'sellingBitcoin' && !isOn) { + $scope.sendStatus = 'success'; + $timeout(function() { + $scope.$digest(); + }, 100); + } else if (showName) { + $scope.sendStatus = showName; + } + }; + + $scope.$on("$ionicView.beforeEnter", function(event, data) { + coinbaseService.setCredentials(); + + amount = data.stateParams.amount; + currency = data.stateParams.currency; + + if (amount < 1) { + showErrorAndBack('Amount must be at least 1.00 ' + currency); + return; + } + + $scope.priceSensitivity = coinbaseService.priceSensitivity; + $scope.selectedPriceSensitivity = { data: coinbaseService.selectedPriceSensitivity }; + + $scope.network = coinbaseService.getNetwork(); + $scope.wallets = profileService.getWallets({ + m: 1, // Only 1-signature wallet + onlyComplete: true, + network: $scope.network + }); + $scope.wallet = $scope.wallets[0]; // Default first wallet + + ongoingProcess.set('connectingCoinbase', true); + coinbaseService.init(function(err, res) { + if (err) { + ongoingProcess.set('connectingCoinbase', false); + showErrorAndBack(err); + return; + } + var accessToken = res.accessToken; + + $scope.paymentMethods = []; + $scope.selectedPaymentMethodId = { value : null }; + coinbaseService.getPaymentMethods(accessToken, function(err, p) { + if (err) { + ongoingProcess.set('connectingCoinbase', false); + showErrorAndBack(err); + return; + } + var hasPrimary; + var pm; + for(var i = 0; i < p.data.length; i++) { + pm = p.data[i]; + if (pm.allow_sell) { + $scope.paymentMethods.push(pm); + } + if (pm.allow_sell && pm.primary_sell) { + hasPrimary = true; + $scope.selectedPaymentMethodId.value = pm.id; + } + } + if (lodash.isEmpty($scope.paymentMethods)) { + ongoingProcess.set('connectingCoinbase', false); + showErrorAndBack('No payment method available to sell'); + return; + } + if (!hasPrimary) $scope.selectedPaymentMethodId.value = $scope.paymentMethods[0].id; + $scope.sellRequest({ quote: true }); + }); + }); + }); + + $scope.sellRequest = function(opts) { + opts = opts || {}; + ongoingProcess.set('connectingCoinbase', true); + coinbaseService.init(function(err, res) { + if (err) { + ongoingProcess.set('connectingCoinbase', false); + showErrorAndBack(err); + return; + } + var accessToken = res.accessToken; + var accountId = res.accountId; + var dataSrc = { + amount: amount, + currency: currency, + payment_method: $scope.selectedPaymentMethodId.value, + commit: opts.commit, + quote: opts.quote + }; + coinbaseService.sellRequest(accessToken, accountId, dataSrc, function(err, data) { + ongoingProcess.set('connectingCoinbase', false); + if (err) { + ongoingProcess.set('connectingCoinbase', false); + showErrorAndBack(err); + return; + } + $scope.sellRequestInfo = data.data; + $timeout(function() { + $scope.$apply(); + }, 100); + }); + }); + }; + + $scope.sellConfirm = function() { + var config = configService.getSync(); + var configWallet = config.wallet; + var walletSettings = configWallet.settings; + + var message = 'Selling bitcoin for ' + amount + ' ' + currency; + var okText = 'Confirm'; + var cancelText = 'Cancel'; + popupService.showConfirm(null, message, okText, cancelText, function(ok) { + if (!ok) return; + + ongoingProcess.set('sellingBitcoin', true, statusChangeHandler); + coinbaseService.init(function(err, res) { + if (err) { + ongoingProcess.set('sellingBitcoin', false, statusChangeHandler); + showError(err); + return; + } + var accessToken = res.accessToken; + var accountId = res.accountId; + + var dataSrc = { + name: 'Received from ' + appConfigService.nameCase + }; + coinbaseService.createAddress(accessToken, accountId, dataSrc, function(err, data) { + if (err) { + ongoingProcess.set('sellingBitcoin', false, statusChangeHandler); + showError(err); + return; + } + var outputs = []; + var toAddress = data.data.address; + var amountSat = parseInt(($scope.sellRequestInfo.amount.amount * 100000000).toFixed(0)); + var comment = 'Sell bitcoin (Coinbase)'; + + outputs.push({ + 'toAddress': toAddress, + 'amount': amountSat, + 'message': comment + }); + + var txp = { + toAddress: toAddress, + amount: amountSat, + outputs: outputs, + message: comment, + payProUrl: null, + excludeUnconfirmedUtxos: configWallet.spendUnconfirmed ? false : true, + feeLevel: walletSettings.feeLevel || 'normal' + }; + + walletService.createTx($scope.wallet, txp, function(err, ctxp) { + if (err) { + ongoingProcess.set('sellingBitcoin', false, statusChangeHandler); + showError(err); + return; + } + $log.debug('Transaction created.'); + publishAndSign($scope.wallet, ctxp, function() {}, function(err, txSent) { + if (err) { + ongoingProcess.set('sellingBitcoin', false, statusChangeHandler); + showError(err); + return; + } + $log.debug('Transaction broadcasted. Wait for Coinbase confirmation...'); + checkTransaction(1, txSent); + }); + }); + }); + }); + }); + }; + + $scope.showWalletSelector = function() { + $scope.walletSelectorTitle = 'Sell From'; + $scope.showWallets = true; + }; + + $scope.onWalletSelect = function(wallet) { + $scope.wallet = wallet; + }; + + $scope.goBackHome = function() { + $scope.sendStatus = ''; + $ionicHistory.nextViewOptions({ + disableAnimate: true, + historyRoot: true + }); + $ionicHistory.clearHistory(); + $state.go('tabs.home').then(function() { + $state.transitionTo('tabs.buyandsell.coinbase'); + }); + }; + +}); diff --git a/src/js/services/coinbaseService.js b/src/js/services/coinbaseService.js index 1e3a3cd33..8d1e9815a 100644 --- a/src/js/services/coinbaseService.js +++ b/src/js/services/coinbaseService.js @@ -368,7 +368,8 @@ angular.module('copayApp.services').factory('coinbaseService', function($http, $ amount: data.amount, currency: data.currency, payment_method: data.payment_method || null, - commit: data.commit || false + commit: data.commit || false, + quote: data.quote || false }; $http(_post('/accounts/' + accountId + '/sells', token, data)).then(function(data) { $log.info('Coinbase Sell Request: SUCCESS'); diff --git a/src/sass/views/integrations/coinbase.scss b/src/sass/views/integrations/coinbase.scss index 0896abffa..5a5531a7f 100644 --- a/src/sass/views/integrations/coinbase.scss +++ b/src/sass/views/integrations/coinbase.scss @@ -1,21 +1,7 @@ -.coinbase-preferences { - ul { - font-size: 14px; - background: white; - li { - padding: 16px 10px 16px 16px; - border-bottom: 1px solid #E9E9EC; - } +#coinbase { + @extend .deflash-blue; + + .add-bottom-for-cta { + bottom: 92px; } } - -.coinbase-last-transactions-content { - background: #fff; - padding: 0.8rem 1rem; - cursor: pointer; - border-bottom: 1px solid #E4E8EC; -} - -.coinbase-pointer { - cursor: pointer; -} diff --git a/www/views/buyCoinbase.html b/www/views/buyCoinbase.html index df6e43ee6..708b55c76 100644 --- a/www/views/buyCoinbase.html +++ b/www/views/buyCoinbase.html @@ -77,6 +77,7 @@
+

Bought

Bitcoin purchase completed. Coinbase has queued the transfer to your selected wallet