From d0dbd85711057483dce77b0080a06db959d53ef8 Mon Sep 17 00:00:00 2001 From: Gustavo Maximiliano Cortez Date: Wed, 13 Apr 2016 14:08:03 -0300 Subject: [PATCH] Feat/coinbase integration (#4012) * Oauth2 and first view * Connect with Coinbase using mobile * Buy and Sell through Coinbase * Fix buy * Receive and send bitcoin to Coinbase account * Receive bitcoin from Coinbase to Copay * Complete user and account information. Connection errors * Improves error handler * Removes console.log * Coinbase background color. Send to Coinbase form validation * Fix send from different wallet * Send and receive using Coinbase * Pagination activity * Fix Buy and Sell * One option in the sidebar to Buy and Sell * Native balance on Coinbase homepage * Rename receive and send * Auto-close window after authenticate * Reorder * Get payment methods * Fix when token expired * Fix token expired * Integration: sell and send to Coinbase * Store pending transaction before sell * Sell flow completed * Removing files * Fix sell * Fix sell * Fix sell * Sell completed * Buy bitcoin through coinbase * Buy auto * Currency set to USD * Select payment methods. Limits * Removes payment methods from preferences * Fix signs. Tx ordered by updated. Minor fixes * Removes console.log * Improving ux-language things * Fix selectedpaymentmethod if not verified * Set error if tx not found * Price sensitivity. Minor fixes * Adds coinbase api key to gitignore * Coinbase production ready * Fix sell in usd * Bug fixes * New Sensitivity step * Refresh token with a simple click * Refresh token * Refactor * Fix auto reconnect if token expired Signed-off-by: Gustavo Maximiliano Cortez * Fix calls if token expired --- .gitignore | 4 + Gruntfile.js | 6 +- public/img/coinbase-logo-inverse.png | Bin 0 -> 2744 bytes public/img/coinbase-logo.png | Bin 0 -> 3347 bytes public/views/buyAndSell.html | 20 + public/views/buyCoinbase.html | 183 +++++++++ public/views/coinbase.html | 186 +++++++++ public/views/coinbaseUri.html | 35 ++ public/views/includes/confirm-tx.html | 2 +- public/views/includes/sidebar.html | 7 +- .../views/modals/coinbase-confirmation.html | 16 + public/views/modals/coinbase-tx-details.html | 107 +++++ .../{glidera-wallets.html => wallets.html} | 4 +- public/views/preferencesCoinbase.html | 60 +++ public/views/preferencesGlobal.html | 12 +- public/views/sellCoinbase.html | 215 +++++++++++ src/css/main.css | 13 +- src/js/controllers/backup.js | 2 +- src/js/controllers/buyCoinbase.js | 206 ++++++++++ src/js/controllers/buyGlidera.js | 2 +- src/js/controllers/coinbase.js | 103 +++++ src/js/controllers/coinbaseUri.js | 38 ++ src/js/controllers/index.js | 258 ++++++++++++- src/js/controllers/preferences.js | 2 +- src/js/controllers/preferencesCoinbase.js | 39 ++ src/js/controllers/preferencesGlobal.js | 12 +- src/js/controllers/sellCoinbase.js | 301 +++++++++++++++ src/js/controllers/sellGlidera.js | 6 +- src/js/controllers/walletHome.js | 4 +- src/js/filters/filters.js | 15 +- src/js/init.js | 4 +- src/js/routes.js | 58 +++ src/js/services/animationService.js | 5 + src/js/services/coinbaseService.js | 365 ++++++++++++++++++ src/js/services/configService.js | 12 + src/js/services/profileService.js | 5 +- src/js/services/storageService.js | 32 ++ src/js/services/txService.js | 61 ++- util/coinbase.js | 20 + 39 files changed, 2365 insertions(+), 55 deletions(-) create mode 100644 public/img/coinbase-logo-inverse.png create mode 100644 public/img/coinbase-logo.png create mode 100644 public/views/buyAndSell.html create mode 100644 public/views/buyCoinbase.html create mode 100644 public/views/coinbase.html create mode 100644 public/views/coinbaseUri.html create mode 100644 public/views/modals/coinbase-confirmation.html create mode 100644 public/views/modals/coinbase-tx-details.html rename public/views/modals/{glidera-wallets.html => wallets.html} (86%) create mode 100644 public/views/preferencesCoinbase.html create mode 100644 public/views/sellCoinbase.html create mode 100644 src/js/controllers/buyCoinbase.js create mode 100644 src/js/controllers/coinbase.js create mode 100644 src/js/controllers/coinbaseUri.js create mode 100644 src/js/controllers/preferencesCoinbase.js create mode 100644 src/js/controllers/sellCoinbase.js create mode 100644 src/js/services/coinbaseService.js create mode 100755 util/coinbase.js diff --git a/.gitignore b/.gitignore index cf8d8b2ea..d92341dbd 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,10 @@ i18n/po/*.mo i18n/crowdin_api_key.txt src/js/translations.js +# Coinbase API ClientID/Secret +coinbase.json +src/js/coinbase.js + # version src/js/version.js diff --git a/Gruntfile.js b/Gruntfile.js index bf463a00b..01406f682 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -7,6 +7,9 @@ module.exports = function(grunt) { version: { command: 'node ./util/version.js' }, + coinbase: { + command: 'node ./util/coinbase.js' + }, clear: { command: 'rm -Rf bower_components node_modules' }, @@ -75,6 +78,7 @@ module.exports = function(grunt) { 'src/js/controllers/*.js', 'src/js/translations.js', 'src/js/version.js', + 'src/js/coinbase.js', 'src/js/init.js', 'src/js/trezor-url.js', 'bower_components/trezor-connect/login.js' @@ -209,7 +213,7 @@ module.exports = function(grunt) { grunt.loadNpmTasks('grunt-node-webkit-builder'); grunt.loadNpmTasks('grunt-contrib-compress'); - grunt.registerTask('default', ['nggettext_compile', 'exec:version', 'browserify', 'concat', 'copy:icons']); + grunt.registerTask('default', ['nggettext_compile', 'exec:version', 'exec:coinbase', 'browserify', 'concat', 'copy:icons']); grunt.registerTask('prod', ['default', 'uglify']); grunt.registerTask('translate', ['nggettext_extract']); grunt.registerTask('test', ['karma:unit']); diff --git a/public/img/coinbase-logo-inverse.png b/public/img/coinbase-logo-inverse.png new file mode 100644 index 0000000000000000000000000000000000000000..b6470219d69aa7921ec9e20c61aa754425d03952 GIT binary patch literal 2744 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 + + +
+ +
diff --git a/public/views/buyCoinbase.html b/public/views/buyCoinbase.html new file mode 100644 index 000000000..ce7eb33dd --- /dev/null +++ b/public/views/buyCoinbase.html @@ -0,0 +1,183 @@ +
+
+ + +
+ +
+
+
+
+
+
+
+
+
+ {{buy.loading}} +
+
+ +
+
+
+
    +
  • +
+
+
+
+ +
+
+ +
+ +
+ + +
+ + + +
+ + + + + BTC + USD +
+ +
+ + Enter the amount to get the exchange rate + + + Not available + + + ~ {{buy.buyPrice.amount * amount | currency : 'USD ' : 2}} + +
+ +
+ +
+ +
+ +
+ + + + +
+
+ +
+ +
+
+
+
+ +
+
+

Funds sent to Copay Wallet

+

+ Buy confirmed. Funds will be send soon to your selected Copay Wallet +

+ +
+
+ +
+

Confirm transaction

+ +
    +
  • + Amount + {{buy.buyInfo.amount.amount}} {{buy.buyInfo.amount.currency}} +
  • +
  • + Fees + +
    + {{fee.type}} {{fee.amount.amount}} {{fee.amount.currency}} +
    +
    +
  • +
  • + Subtotal + {{buy.buyInfo.subtotal.amount}} {{buy.buyInfo.subtotal.currency}} +
  • +
  • + Total + {{buy.buyInfo.total.amount}} {{buy.buyInfo.total.currency}} +
  • +
  • + Payout at + {{buy.buyInfo.payout_at | amCalendar}} +
  • +
  • + Deposit into Copay Wallet + {{buy.selectedWalletName}} +
  • +
+
+
+ +
+
+
+
+
+

Purchase initiated

+

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

+ + +
+
+ +
+
diff --git a/public/views/coinbase.html b/public/views/coinbase.html new file mode 100644 index 000000000..9883212fc --- /dev/null +++ b/public/views/coinbase.html @@ -0,0 +1,186 @@ + +
+ +
+ +
+ +
+
+
+
+
+
+
+
+
+ Connecting to Coinbase... + {{index.coinbaseLoading}} +
+
+ +
+
+
    +
  • +
+
+
+
+ Your primary account should be a WALLET. Set your wallet account as primary and try again. +
+
+
+ +
+ Or go to Preferences and log out manually. +
+
+
+ +
+
+ + Testnet wallets only work with Coinbase Sandbox Accounts +
+
+
+ +
+
+ +

Connect your Coinbase account to get started

+ + + Connect to Coinbase + + +
+
+
+
    +
  • +
+
+
+ + + +
+ +
+
+
+ +
+ +
+ +
+ +
    +
  • + buy bitcoin + Buy Bitcoin + + + +
  • +
  • + sell bitcoin + Sell Bitcoin + + + +
  • +
+ +
+

Activity

+
+
    +
  • +
+
+
+
+ bought + bought + sold + sold +
+ +
+
+ Sold + Bought + + -{{tx.amount.amount.replace('-','')}} + {{tx.amount.currency}} + +
+
+
+
+ Error +
+
+
+ +
+
+ Pending +
+
+
+
+ +
+
+
+ +
+ +
+
diff --git a/public/views/coinbaseUri.html b/public/views/coinbaseUri.html new file mode 100644 index 000000000..b5475440e --- /dev/null +++ b/public/views/coinbaseUri.html @@ -0,0 +1,35 @@ +
+
+ +
+ +
+
+
+
+
+
+
+
+
+ Connecting to Coinbase... +
+
+ +
+
+
+ +
+ +
+
{{coinbase.error}}
+ +
+
+
+
diff --git a/public/views/includes/confirm-tx.html b/public/views/includes/confirm-tx.html index 239ea591a..57f348423 100644 --- a/public/views/includes/confirm-tx.html +++ b/public/views/includes/confirm-tx.html @@ -3,7 +3,7 @@
-

Confirm transaction

+

Send bitcoin

{{tx.amountStr}}
diff --git a/public/views/includes/sidebar.html b/public/views/includes/sidebar.html index 3f2d93506..752e3cbbb 100644 --- a/public/views/includes/sidebar.html +++ b/public/views/includes/sidebar.html @@ -28,14 +28,13 @@
Create, join or import
-
  • - +
  • + -
  • diff --git a/public/views/modals/coinbase-confirmation.html b/public/views/modals/coinbase-confirmation.html new file mode 100644 index 000000000..ad8e7207b --- /dev/null +++ b/public/views/modals/coinbase-confirmation.html @@ -0,0 +1,16 @@ +
    +
    +

    Are you sure you would like to log out of your Coinbase account?

    +

    You will need to log back in to buy or sell bitcoin in Copay.

    +
    + +
    +
    + +
    +
    +
    diff --git a/public/views/modals/coinbase-tx-details.html b/public/views/modals/coinbase-tx-details.html new file mode 100644 index 000000000..c3252dfb4 --- /dev/null +++ b/public/views/modals/coinbase-tx-details.html @@ -0,0 +1,107 @@ + + + diff --git a/public/views/modals/glidera-wallets.html b/public/views/modals/wallets.html similarity index 86% rename from public/views/modals/glidera-wallets.html rename to public/views/modals/wallets.html index 52b4d55e3..47fb14d9a 100644 --- a/public/views/modals/glidera-wallets.html +++ b/public/views/modals/wallets.html @@ -7,8 +7,8 @@

    - Choose your destination wallet - Choose your source wallet + Choose your destination wallet + Choose your source wallet

    diff --git a/public/views/preferencesCoinbase.html b/public/views/preferencesCoinbase.html new file mode 100644 index 000000000..55fc293a2 --- /dev/null +++ b/public/views/preferencesCoinbase.html @@ -0,0 +1,60 @@ +
    +
    + +
    + +
      +

      Account

      +
    • + ID + + {{index.coinbaseAccount.id}} + +
    • +
    • + Name + + {{index.coinbaseAccount.name}} + +
    • +
    • + Balance + + {{index.coinbaseAccount.balance.amount}} {{index.coinbaseAccount.balance.currency}} + +
    • +
    • + Native Balance + + {{index.coinbaseAccount.native_balance.amount}} {{index.coinbaseAccount.native_balance.currency}} + +
    • + +

      User Information

      +
    • + ID + + {{index.coinbaseUser.id}} + +
    • +
    • + Email + + {{index.coinbaseUser.email}} + +
    • +
    +
      +

      +
    • + + Log out +
    • +
    +

    + +
    +
    diff --git a/public/views/preferencesGlobal.html b/public/views/preferencesGlobal.html index 2b80961e5..0af10d193 100644 --- a/public/views/preferencesGlobal.html +++ b/public/views/preferencesGlobal.html @@ -63,13 +63,13 @@
    Enable Glidera Service
  • - -

      diff --git a/public/views/sellCoinbase.html b/public/views/sellCoinbase.html new file mode 100644 index 000000000..a9450325e --- /dev/null +++ b/public/views/sellCoinbase.html @@ -0,0 +1,215 @@ +
      +
      + + +
      + +
      +
      +
      +
      +
      +
      +
      +
      +
      + {{sell.loading}} +
      +
      + +
      +
      +
      +
        +
      • +
      +
      +
      +
      + +
      +
      +
      + + +
      + +
      + +
      + + + + +
      +
      + + +
      + + + + + BTC + USD +
      + +
      + + Enter the amount to get the exchange rate + + + Not available + + + ~ {{sell.sellPrice.amount * amount | currency : 'USD ' : 2}} + +
      + +
      + +
      + +
      + + +
      +
      + Continue +
      +
      + +
      +

      Price Sensitivity

      +

      + Coinbase has not yet implemented an immediate method to sell bitcoin from a wallet. To make this sale, funds + will be sent to your Coinbase account, and sold when Coinbase accepts the transaction (usually one + hour). +

      + + +

      + Estimated sale value: {{sell.sellPrice.amount * amount | currency : 'USD ' : 2}}
      + Still sell if price fall until: + {{(sell.sellPrice.amount - (selectedPriceSensitivity.value / 100) * sell.sellPrice.amount) * amount | currency : 'USD ' : 2}} +

      + +
      +
      + Back +
      +
      + +
      +
      +
      +
      +
      +
      + +
      +
      +

      Funds sent to Coinbase Account

      +

      + The transaction is not yet confirmed, and will show as "Processing" in your Activity. The bitcoin sale will be completed automatically once it is confirmed by Coinbase. +

      + +
      +
      + +
      +

      Confirm transaction

      + +
        +
      • + Amount + {{sell.sellInfo.amount.amount}} {{sell.sellInfo.amount.currency}} +
      • +
      • + Fees + +
        + {{fee.type}} {{fee.amount.amount}} {{fee.amount.currency}} +
        +
        +
      • +
      • + Subtotal + {{sell.sellInfo.subtotal.amount}} {{sell.sellInfo.subtotal.currency}} +
      • +
      • + Total + {{sell.sellInfo.total.amount}} {{sell.sellInfo.total.currency}} +
      • +
      • + Payout at + {{sell.sellInfo.payout_at | amCalendar}} +
      • +
      +
      +
      + +
      +
      +
      +
      +
      +

      Sale initiated

      +

      + A transfer has been initiated to your bank account and should arrive at {{sell.success.payout_at | amCalendar}}. +

      + + +
      +
      + +
      +
      diff --git a/src/css/main.css b/src/css/main.css index d0bdf0f90..85f02843e 100644 --- a/src/css/main.css +++ b/src/css/main.css @@ -73,7 +73,7 @@ h4.title a { } -.preferences h4, .modal-content h4, .glidera h4, .txModal h4 { +.preferences h4, .modal-content h4, .glidera h4, .coinbase h4, .txModal h4 { background: #F6F7F9; padding: 25px 0px 5px 10px; text-transform: uppercase; @@ -111,6 +111,17 @@ h4.title a { padding-top: 7px; } +.disabled-input { + display: block; + margin-bottom: 1.5rem; + background-color: #E4E8EC; + padding-left: 0.5rem; + color: #2C3E50; + font-size: 13px; + height: 35px; + padding-top: 7px; +} + ul.button-group { margin-top: 8px; } diff --git a/src/js/controllers/backup.js b/src/js/controllers/backup.js index d581f8295..c5653b3e9 100644 --- a/src/js/controllers/backup.js +++ b/src/js/controllers/backup.js @@ -74,7 +74,7 @@ angular.module('copayApp.controllers').controller('backupController', $scope.$apply(); }, 1); - profileService.unlockFC(function(err) { + profileService.unlockFC({}, function(err) { if (err) { self.error = bwsError.msg(err, gettext('Could not decrypt')); $log.warn('Error decrypting credentials:', self.error); //TODO diff --git a/src/js/controllers/buyCoinbase.js b/src/js/controllers/buyCoinbase.js new file mode 100644 index 000000000..5004529cd --- /dev/null +++ b/src/js/controllers/buyCoinbase.js @@ -0,0 +1,206 @@ +'use strict'; + +angular.module('copayApp.controllers').controller('buyCoinbaseController', + function($scope, $modal, $log, $timeout, lodash, profileService, coinbaseService, animationService, txService, bwsError, addressService) { + + window.ignoreMobilePause = true; + var self = this; + var fc; + + var otherWallets = function(testnet) { + var network = testnet ? 'testnet' : 'livenet'; + return lodash.filter(profileService.getWallets(network), function(w) { + return w.network == network && w.m == 1; + }); + }; + + this.init = function(testnet) { + self.otherWallets = otherWallets(testnet); + // Choose focused wallet + try { + var currentWalletId = profileService.focusedClient.credentials.walletId; + lodash.find(self.otherWallets, function(w) { + if (w.id == currentWalletId) { + $timeout(function() { + self.selectedWalletId = w.id; + self.selectedWalletName = w.name; + fc = profileService.getClient(w.id); + $scope.$apply(); + }, 100); + } + }); + } catch (e) { + $log.debug(e); + }; + }; + + this.getPaymentMethods = function(token) { + coinbaseService.getPaymentMethods(token, function(err, p) { + if (err) { + self.error = err; + return; + } + self.paymentMethods = []; + lodash.each(p.data, function(pm) { + if (pm.allow_buy) { + self.paymentMethods.push(pm); + } + if (pm.allow_buy && pm.primary_buy) { + $scope.selectedPaymentMethod = pm; + } + }); + }); + }; + + this.getPrice = function(token) { + var currency = 'USD'; + coinbaseService.buyPrice(token, currency, function(err, b) { + if (err) return; + self.buyPrice = b.data || null; + }); + }; + + $scope.openWalletsModal = function(wallets) { + self.error = null; + var ModalInstanceCtrl = function($scope, $modalInstance) { + $scope.type = 'BUY'; + $scope.wallets = wallets; + $scope.noColor = true; + $scope.cancel = function() { + $modalInstance.dismiss('cancel'); + }; + + $scope.selectWallet = function(walletId, walletName) { + if (!profileService.getClient(walletId).isComplete()) { + self.error = bwsError.msg({ + 'code': 'WALLET_NOT_COMPLETE' + }, 'Could not choose the wallet'); + $modalInstance.dismiss('cancel'); + return; + } + $modalInstance.close({ + 'walletId': walletId, + 'walletName': walletName, + }); + }; + }; + + var modalInstance = $modal.open({ + templateUrl: 'views/modals/wallets.html', + windowClass: animationService.modalAnimated.slideUp, + controller: ModalInstanceCtrl, + }); + + modalInstance.result.finally(function() { + var m = angular.element(document.getElementsByClassName('reveal-modal')); + m.addClass(animationService.modalAnimated.slideOutDown); + }); + + modalInstance.result.then(function(obj) { + $timeout(function() { + self.selectedWalletId = obj.walletId; + self.selectedWalletName = obj.walletName; + fc = profileService.getClient(obj.walletId); + $scope.$apply(); + }, 100); + }); + }; + + this.buyRequest = function(token, account) { + self.error = null; + var accountId = account.id; + var amount = $scope.amount ? $scope.amount : $scope.fiat; + var currency = $scope.amount ? 'BTC' : 'USD'; + if (!amount) return; + var dataSrc = { + amount: amount, + currency: currency, + payment_method: $scope.selectedPaymentMethod.id || null + }; + this.loading = 'Sending request...'; + coinbaseService.buyRequest(token, accountId, dataSrc, function(err, data) { + self.loading = null; + if (err) { + self.error = err; + return; + } + self.buyInfo = data.data; + }); + }; + + this.confirmBuy = function(token, account, buy) { + self.error = null; + var accountId = account.id; + var buyId = buy.id; + this.loading = 'Buying bitcoin...'; + coinbaseService.buyCommit(token, accountId, buyId, function(err, b) { + self.loading = null; + if (err) { + self.error = err; + return; + } else { + var tx = b.data.transaction; + if (!tx) return; + + self.loading = 'Getting transaction...'; + coinbaseService.getTransaction(token, accountId, tx.id, function(err, updatedTx) { + if (err) $log.debug(err); + addressService.getAddress(self.selectedWalletId, false, function(err, addr) { + if (err) { + self.loading = null; + self.error = {errors: [{ message: 'Could not create address' }]}; + return; + } + coinbaseService.savePendingTransaction(updatedTx.data, {toAddr: addr}, function(err) { + self.loading = null; + if (err) $log.debug(err); + updatedTx.data['toAddr'] = addr; + if (updatedTx.data.status == 'completed') { + self.sendToCopay(token, account, updatedTx.data); + } else { + self.success = updatedTx.data; + $timeout(function() { + $scope.$emit('Local/CoinbaseTx'); + }, 1000); + } + }); + }); + }); + } + }); + }; + + this.sendToCopay = function(token, account, tx) { + self.error = null; + var accountId = account.id; + + self.loading = 'Sending funds to Copay...'; + var data = { + to: tx.toAddr, + amount: tx.amount.amount, + currency: tx.amount.currency, + description: 'Copay Wallet: ' + self.selectedWalletName + }; + coinbaseService.sendTo(token, accountId, data, function(err, res) { + self.loading = null; + if (err) { + self.error = err; + } else { + self.receiveInfo = res.data; + if (!res.data.id) return; + coinbaseService.getTransaction(token, accountId, res.data.id, function(err, sendTx) { + coinbaseService.savePendingTransaction(tx, {remove: true}, function(err) { + coinbaseService.savePendingTransaction(sendTx.data, {}, function(err) { + $timeout(function() { + $scope.$emit('Local/CoinbaseTx'); + }, 1000); + }); + }); + }); + } + + }); + }; + + + }); diff --git a/src/js/controllers/buyGlidera.js b/src/js/controllers/buyGlidera.js index b6777f326..d3e163e8c 100644 --- a/src/js/controllers/buyGlidera.js +++ b/src/js/controllers/buyGlidera.js @@ -63,7 +63,7 @@ angular.module('copayApp.controllers').controller('buyGlideraController', }; var modalInstance = $modal.open({ - templateUrl: 'views/modals/glidera-wallets.html', + templateUrl: 'views/modals/wallets.html', windowClass: animationService.modalAnimated.slideUp, controller: ModalInstanceCtrl, }); diff --git a/src/js/controllers/coinbase.js b/src/js/controllers/coinbase.js new file mode 100644 index 000000000..f9e9c3b5f --- /dev/null +++ b/src/js/controllers/coinbase.js @@ -0,0 +1,103 @@ +'use strict'; + +angular.module('copayApp.controllers').controller('coinbaseController', + function($rootScope, $scope, $timeout, $modal, profileService, configService, storageService, coinbaseService, isChromeApp, animationService, lodash, nodeWebkit) { + + this.openAuthenticateWindow = function() { + var oauthUrl = this.getAuthenticateUrl(); + if (!nodeWebkit.isDefined()) { + $rootScope.openExternalLink(oauthUrl, '_system'); + } else { + var self = this; + var gui = require('nw.gui'); + var win = gui.Window.open(oauthUrl, { + focus: true, + position: 'center' + }); + win.on ('loaded', function(){ + var title = win.title; + if (title.indexOf('Coinbase') == -1) { + $scope.code = title; + self.submitOauthCode(title); + win.close(); + } + }); + } + } + + this.getAuthenticateUrl = function() { + return coinbaseService.getOauthCodeUrl(); + }; + + this.submitOauthCode = function(code) { + var self = this; + var coinbaseTestnet = configService.getSync().coinbase.testnet; + var network = coinbaseTestnet ? 'testnet' : 'livenet'; + this.loading = true; + this.error = null; + $timeout(function() { + coinbaseService.getToken(code, function(err, data) { + self.loading = null; + if (err) { + self.error = err; + $timeout(function() { + $scope.$apply(); + }, 100); + } + else if (data && data.access_token && data.refresh_token) { + storageService.setCoinbaseToken(network, data.access_token, function() { + storageService.setCoinbaseRefreshToken(network, data.refresh_token, function() { + $scope.$emit('Local/CoinbaseUpdated', data.access_token); + $timeout(function() { + $scope.$apply(); + }, 100); + }); + }); + } + }); + }, 100); + }; + + this.openTxModal = function(tx) { + $rootScope.modalOpened = true; + var self = this; + var config = configService.getSync().wallet.settings; + var fc = profileService.focusedClient; + var ModalInstanceCtrl = function($scope, $modalInstance) { + $scope.tx = tx; + $scope.settings = config; + $scope.color = fc.backgroundColor; + $scope.noColor = true; + + $scope.remove = function() { + coinbaseService.savePendingTransaction($scope.tx, {remove: true}, function(err) { + $rootScope.$emit('Local/CoinbaseTx'); + $scope.cancel(); + }); + }; + + $scope.cancel = lodash.debounce(function() { + $modalInstance.dismiss('cancel'); + }, 0, 1000); + + }; + + var modalInstance = $modal.open({ + templateUrl: 'views/modals/coinbase-tx-details.html', + windowClass: animationService.modalAnimated.slideRight, + controller: ModalInstanceCtrl, + }); + + var disableCloseModal = $rootScope.$on('closeModal', function() { + modalInstance.dismiss('cancel'); + }); + + modalInstance.result.finally(function() { + $rootScope.modalOpened = false; + disableCloseModal(); + var m = angular.element(document.getElementsByClassName('reveal-modal')); + m.addClass(animationService.modalAnimated.slideOutRight); + }); + }; + + }); diff --git a/src/js/controllers/coinbaseUri.js b/src/js/controllers/coinbaseUri.js new file mode 100644 index 000000000..7c607794d --- /dev/null +++ b/src/js/controllers/coinbaseUri.js @@ -0,0 +1,38 @@ +'use strict'; +angular.module('copayApp.controllers').controller('coinbaseUriController', + function($scope, $stateParams, $timeout, profileService, configService, coinbaseService, storageService, go) { + + this.submitOauthCode = function(code) { + var self = this; + var coinbaseTestnet = configService.getSync().coinbase.testnet; + var network = coinbaseTestnet ? 'testnet' : 'livenet'; + this.loading = true; + this.error = null; + $timeout(function() { + coinbaseService.getToken(code, function(err, data) { + self.loading = null; + if (err) { + self.error = err; + $timeout(function() { + $scope.$apply(); + }, 100); + } + else if (data && data.access_token) { + storageService.setCoinbaseToken(network, data.access_token, function() { + $scope.$emit('Local/CoinbaseUpdated', data.access_token); + $timeout(function() { + go.path('coinbase'); + $scope.$apply(); + }, 100); + }); + } + }); + }, 100); + }; + + this.checkCode = function() { + this.code = $stateParams.code; + this.submitOauthCode(this.code); + }; + + }); diff --git a/src/js/controllers/index.js b/src/js/controllers/index.js index 504637d86..902f5b795 100644 --- a/src/js/controllers/index.js +++ b/src/js/controllers/index.js @@ -1,6 +1,6 @@ 'use strict'; -angular.module('copayApp.controllers').controller('indexController', function($rootScope, $scope, $log, $filter, $timeout, latestReleaseService, bwcService, pushNotificationsService, lodash, go, profileService, configService, isCordova, rateService, storageService, addressService, gettext, gettextCatalog, amMoment, nodeWebkit, addonManager, isChromeApp, bwsError, txFormatService, uxLanguage, $state, glideraService, isMobile, addressbookService) { +angular.module('copayApp.controllers').controller('indexController', function($rootScope, $scope, $log, $filter, $timeout, latestReleaseService, bwcService, pushNotificationsService, lodash, go, profileService, configService, isCordova, rateService, storageService, addressService, gettext, gettextCatalog, amMoment, nodeWebkit, addonManager, isChromeApp, bwsError, txFormatService, uxLanguage, $state, glideraService, coinbaseService, isMobile, addressbookService) { var self = this; var SOFT_CONFIRMATION_LIMIT = 12; var errors = bwcService.getErrors(); @@ -147,6 +147,7 @@ angular.module('copayApp.controllers').controller('indexController', function($r self.setAddressbook(); self.initGlidera(); + self.initCoinbase(); self.setCustomBWSFlag(); @@ -1265,6 +1266,246 @@ angular.module('copayApp.controllers').controller('indexController', function($r }; + self.initCoinbase = function(accessToken) { + self.coinbaseEnabled = configService.getSync().coinbase.enabled; + self.coinbaseTestnet = configService.getSync().coinbase.testnet; + var network = self.coinbaseTestnet ? 'testnet' : 'livenet'; + + self.coinbaseToken = null; + self.coinbaseError = null; + self.coinbasePermissions = null; + self.coinbaseEmail = null; + self.coinbasePersonalInfo = null; + self.coinbaseTxs = null; + self.coinbaseStatus = null; + + if (!self.coinbaseEnabled) return; + + coinbaseService.setCredentials(network); + + var getToken = function(cb) { + if (accessToken) { + cb(null, accessToken); + } else { + storageService.getCoinbaseToken(network, cb); + } + }; + + getToken(function(err, accessToken) { + if (err || !accessToken) return; + else { + self.coinbaseLoading = 'Getting primary account...'; + coinbaseService.getAccounts(accessToken, function(err, a) { + self.coinbaseLoading = null; + if (err) { + self.coinbaseError = err; + if (err.errors[0] && err.errors[0].id == 'expired_token') { + self.refreshCoinbaseToken(); + } + } else { + self.coinbaseToken = accessToken; + lodash.each(a.data, function(account) { + if (account.primary && account.type == 'wallet') { + self.coinbaseAccount = account; + self.updateCoinbase(); + } + }); + } + }); + } + }); + }; + + self.updateCoinbase = lodash.debounce(function(opts) { + if (!self.coinbaseToken || !self.coinbaseAccount) return; + var accessToken = self.coinbaseToken; + var accountId = self.coinbaseAccount.id; + + opts = opts || {}; + + if (opts.updateAccount) { + coinbaseService.getAccount(accessToken, accountId, function(err, a) { + if (err) { + self.coinbaseError = err; + if (err.errors[0] && err.errors[0].id == 'expired_token') { + self.refreshCoinbaseToken(); + } + return; + } + self.coinbaseAccount = a.data; + }); + } + + coinbaseService.getCurrentUser(accessToken, function(err, u) { + if (err) { + self.coinbaseError = err; + if (err.errors[0] && err.errors[0].id == 'expired_token') { + self.refreshCoinbaseToken(); + } + return; + } + self.coinbaseUser = u.data; + }); + + coinbaseService.getPendingTransactions(function(err, txs) { + self.coinbasePendingTransactions = lodash.isEmpty(txs) ? null : txs; + lodash.forEach(txs, function(dataFromStorage, txId) { + if ((dataFromStorage.type == 'sell' && dataFromStorage.status == 'completed') || + (dataFromStorage.type == 'buy' && dataFromStorage.status == 'completed') || + dataFromStorage.status == 'error' || + (dataFromStorage.type == 'send' && dataFromStorage.status == 'completed')) return; + coinbaseService.getTransaction(accessToken, accountId, txId, function(err, tx) { + if (err) { + if (err.errors[0] && err.errors[0].id == 'expired_token') { + self.refreshCoinbaseToken(); + return; + } + coinbaseService.savePendingTransaction(dataFromStorage, {status: 'error', error: err}, function(err) { + if (err) $log.debug(err); + }); + return; + } + _updateCoinbasePendingTransactions(dataFromStorage, tx.data); + self.coinbasePendingTransactions[txId] = dataFromStorage; + if (tx.data.type == 'send' && tx.data.status == 'completed' && tx.data.from) { + coinbaseService.sellPrice(accessToken, dataFromStorage.sell_price_currency, function(err, s) { + if (err) { + if (err.errors[0] && err.errors[0].id == 'expired_token') { + self.refreshCoinbaseToken(); + return; + } + coinbaseService.savePendingTransaction(dataFromStorage, {status: 'error', error: err}, function(err) { + if (err) $log.debug(err); + }); + return; + } + var newSellPrice = s.data.amount; + var variance = Math.abs((newSellPrice - dataFromStorage.sell_price_amount) / dataFromStorage.sell_price_amount * 100); + if (variance < dataFromStorage.price_sensitivity.value) { + self.sellPending(tx.data); + } else { + var error = {errors: [{ message: 'Price falls over the selected percentage' }]}; + coinbaseService.savePendingTransaction(dataFromStorage, {status: 'error', error: error}, function(err) { + if (err) $log.debug(err); + }); + } + }); + } else if (tx.data.type == 'buy' && tx.data.status == 'pending' && tx.data.buy) { + self.buyPending(tx.data); + } else { + coinbaseService.savePendingTransaction(dataFromStorage, {}, function(err) { + if (err) $log.debug(err); + }); + } + }); + }); + }); + + }, 1000); + + var _updateCoinbasePendingTransactions = function (obj/*, …*/) { + for (var i=1; i b[field] ? 1 : -1); + }); + if(reverse) filtered.reverse(); + return filtered; + }; +}); diff --git a/src/js/init.js b/src/js/init.js index 0bc31fddf..f0723e7fc 100644 --- a/src/js/init.js +++ b/src/js/init.js @@ -11,6 +11,8 @@ angular.element(document).ready(function() { if (!url) return; if (url.indexOf('glidera') != -1) { url = '#/uri-glidera' + url.replace('bitcoin://glidera', ''); + } else if (url.indexOf('coinbase') != -1) { + url = '#/uri-coinbase' + url.replace('bitcoin://coinbase', ''); } else { url = '#/uri-payment/' + url; } @@ -46,7 +48,7 @@ angular.element(document).ready(function() { } setTimeout(function() { var loc = window.location; - var ignoreMobilePause = loc.toString().match(/(buy|sell)/) ? true : false; + var ignoreMobilePause = loc.toString().match(/(buy|sell|buycoinbase|sellcoinbase)/) ? true : false; window.ignoreMobilePause = ignoreMobilePause; }, 100); }, false); diff --git a/src/js/routes.js b/src/js/routes.js index 134bca2e7..229552ba2 100644 --- a/src/js/routes.js +++ b/src/js/routes.js @@ -269,6 +269,64 @@ angular }, } }) + .state('coinbase', { + url: '/coinbase', + walletShouldBeComplete: true, + needProfile: true, + views: { + 'main': { + templateUrl: 'views/coinbase.html' + }, + } + }) + .state('preferencesCoinbase', { + url: '/preferencesCoinbase', + walletShouldBeComplete: true, + needProfile: true, + views: { + 'main': { + templateUrl: 'views/preferencesCoinbase.html' + }, + } + }) + .state('uricoinbase', { + url: '/uri-coinbase?code', + needProfile: true, + views: { + 'main': { + templateUrl: 'views/coinbaseUri.html' + }, + } + }) + .state('buyCoinbase', { + url: '/buycoinbase', + walletShouldBeComplete: true, + needProfile: true, + views: { + 'main': { + templateUrl: 'views/buyCoinbase.html' + }, + } + }) + .state('sellCoinbase', { + url: '/sellcoinbase', + walletShouldBeComplete: true, + needProfile: true, + views: { + 'main': { + templateUrl: 'views/sellCoinbase.html' + }, + } + }) + .state('buyandsell', { + url: '/buyandsell', + needProfile: true, + views: { + 'main': { + templateUrl: 'views/buyAndSell.html' + }, + } + }) .state('preferencesAdvanced', { url: '/preferencesAdvanced', templateUrl: 'views/preferencesAdvanced.html', diff --git a/src/js/services/animationService.js b/src/js/services/animationService.js index dc43de45d..d038043b7 100644 --- a/src/js/services/animationService.js +++ b/src/js/services/animationService.js @@ -23,12 +23,16 @@ angular.module('copayApp.services').factory('animationService', function(isCordo preferences: 11, preferencesGlobal: 11, glidera: 11, + coinbase: 11, preferencesColor: 12, backup: 12, preferencesAdvanced: 12, buyGlidera: 12, + buyCoinbase: 12, sellGlidera: 12, + sellCoinbase: 12, preferencesGlidera: 12, + preferencesCoinbase: 12, about: 12, delete: 13, preferencesLanguage: 12, @@ -46,6 +50,7 @@ angular.module('copayApp.services').factory('animationService', function(isCordo termOfUse: 13, translators: 13, add: 11, + buyandsell: 11, create: 12, join: 12, import: 12, diff --git a/src/js/services/coinbaseService.js b/src/js/services/coinbaseService.js new file mode 100644 index 000000000..052bdf32f --- /dev/null +++ b/src/js/services/coinbaseService.js @@ -0,0 +1,365 @@ +'use strict'; + +angular.module('copayApp.services').factory('coinbaseService', function($http, $log, isCordova, lodash, storageService, configService) { + var root = {}; + var credentials = {}; + + root.setCredentials = function(network) { + credentials.SCOPE = '' + + 'wallet:accounts:read,' + + 'wallet:addresses:read,' + + 'wallet:addresses:create,' + + 'wallet:user:read,' + + 'wallet:user:email,' + + 'wallet:buys:read,' + + 'wallet:buys:create,' + + 'wallet:sells:read,' + + 'wallet:sells:create,' + + 'wallet:transactions:read,' + + 'wallet:transactions:send,' + + 'wallet:payment-methods:read'; + + if (isCordova) { + credentials.REDIRECT_URI = 'bitcoin://coinbase'; + } else { + credentials.REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob'; + } + + if (network == 'testnet') { + credentials.HOST = 'https://sandbox.coinbase.com'; + credentials.API = 'https://api.sandbox.coinbase.com'; + credentials.CLIENT_ID = '6cdcc82d5d46654c46880e93ab3d2a43c639776347dd88022904bd78cd067841'; + credentials.CLIENT_SECRET = '228cb6308951f4b6f41ba010c7d7981b2721a493c40c50fd2425132dcaccce59'; + } + else { + credentials.HOST = 'https://coinbase.com'; + credentials.API = 'https://api.coinbase.com'; + credentials.CLIENT_ID = window.coinbase_client_id; + credentials.CLIENT_SECRET = window.coinbase_client_secret; + }; + }; + + root.getOauthCodeUrl = function() { + return credentials.HOST + + '/oauth/authorize?response_type=code&client_id=' + + credentials.CLIENT_ID + + '&redirect_uri=' + + credentials.REDIRECT_URI + + '&state=SECURE_RANDOM&scope=' + + credentials.SCOPE + + '&meta[send_limit_amount]=100&meta[send_limit_currency]=USD&meta[send_limit_period]=day'; + }; + + root.getToken = function(code, cb) { + var req = { + method: 'POST', + url: credentials.API + '/oauth/token', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + data: { + grant_type : 'authorization_code', + code: code, + client_id : credentials.CLIENT_ID, + client_secret: credentials.CLIENT_SECRET, + redirect_uri: credentials.REDIRECT_URI + } + }; + + $http(req).then(function(data) { + $log.info('Coinbase Authorization Access Token: SUCCESS'); + return cb(null, data.data); + }, function(data) { + $log.error('Coinbase Authorization Access Token: ERROR ' + data.statusText); + return cb(data.data); + }); + }; + + root.refreshToken = function(refreshToken, cb) { + var req = { + method: 'POST', + url: credentials.API + '/oauth/token', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + data: { + grant_type : 'refresh_token', + client_id : credentials.CLIENT_ID, + client_secret: credentials.CLIENT_SECRET, + redirect_uri: credentials.REDIRECT_URI, + refresh_token: refreshToken + } + }; + + $http(req).then(function(data) { + $log.info('Coinbase Refresh Access Token: SUCCESS'); + return cb(null, data.data); + }, function(data) { + $log.error('Coinbase Refresh Access Token: ERROR ' + data.statusText); + return cb(data.data); + }); + }; + + var _get = function(endpoint, token) { + return { + method: 'GET', + url: credentials.API + '/v2' + endpoint, + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer ' + token + } + }; + }; + + root.getAccounts = function(token, cb) { + if (!token) return cb('Invalid Token'); + $http(_get('/accounts', token)).then(function(data) { + $log.info('Coinbase Get Accounts: SUCCESS'); + return cb(null, data.data); + }, function(data) { + $log.error('Coinbase Get Accounts: ERROR ' + data.statusText); + return cb(data.data); + }); + }; + + root.getAccount = function(token, accountId, cb) { + if (!token) return cb('Invalid Token'); + $http(_get('/accounts/' + accountId, token)).then(function(data) { + $log.info('Coinbase Get Account: SUCCESS'); + return cb(null, data.data); + }, function(data) { + $log.error('Coinbase Get Account: ERROR ' + data.statusText); + return cb(data.data); + }); + }; + + root.getAuthorizationInformation = function(token, cb) { + if (!token) return cb('Invalid Token'); + $http(_get('/user/auth', token)).then(function(data) { + $log.info('Coinbase Autorization Information: SUCCESS'); + return cb(null, data.data); + }, function(data) { + $log.error('Coinbase Autorization Information: ERROR ' + data.statusText); + return cb(data.data); + }); + }; + + root.getCurrentUser = function(token, cb) { + if (!token) return cb('Invalid Token'); + $http(_get('/user', token)).then(function(data) { + $log.info('Coinbase Get Current User: SUCCESS'); + return cb(null, data.data); + }, function(data) { + $log.error('Coinbase Get Current User: ERROR ' + data.statusText); + return cb(data.data); + }); + }; + + root.getTransaction = function(token, accountId, transactionId, cb) { + if (!token) return cb('Invalid Token'); + $http(_get('/accounts/' + accountId + '/transactions/' + transactionId, token)).then(function(data) { + $log.info('Coinbase Transaction: SUCCESS'); + return cb(null, data.data); + }, function(data) { + $log.error('Coinbase Transaction: ERROR ' + data.statusText); + return cb(data.data); + }); + }; + + root.getTransactions = function(token, accountId, cb) { + if (!token) return cb('Invalid Token'); + $http(_get('/accounts/' + accountId + '/transactions', token)).then(function(data) { + $log.info('Coinbase Transactions: SUCCESS'); + return cb(null, data.data); + }, function(data) { + $log.error('Coinbase Transactions: ERROR ' + data.statusText); + return cb(data.data); + }); + }; + + root.paginationTransactions = function(token, Url, cb) { + if (!token) return cb('Invalid Token'); + $http(_get(Url.replace('/v2', ''), token)).then(function(data) { + $log.info('Coinbase Pagination Transactions: SUCCESS'); + return cb(null, data.data); + }, function(data) { + $log.error('Coinbase Pagination Transactions: ERROR ' + data.statusText); + return cb(data.data); + }); + }; + + root.sellPrice = function(token, currency, cb) { + $http(_get('/prices/sell?currency=' + currency, token)).then(function(data) { + $log.info('Coinbase Sell Price: SUCCESS'); + return cb(null, data.data); + }, function(data) { + $log.error('Coinbase Sell Price: ERROR ' + data.statusText); + return cb(data.data); + }); + }; + + root.buyPrice = function(token, currency, cb) { + $http(_get('/prices/buy?currency=' + currency, token)).then(function(data) { + $log.info('Coinbase Buy Price: SUCCESS'); + return cb(null, data.data); + }, function(data) { + $log.error('Coinbase Buy Price: ERROR ' + data.statusText); + return cb(data.data); + }); + }; + + root.getPaymentMethods = function(token, cb) { + $http(_get('/payment-methods', token)).then(function(data) { + $log.info('Coinbase Get Payment Methods: SUCCESS'); + return cb(null, data.data); + }, function(data) { + $log.error('Coinbase Get Payment Methods: ERROR ' + data.statusText); + return cb(data.data); + }); + }; + + root.getPaymentMethod = function(token, paymentMethodId, cb) { + $http(_get('/payment-methods/' + paymentMethodId, token)).then(function(data) { + $log.info('Coinbase Get Payment Method: SUCCESS'); + return cb(null, data.data); + }, function(data) { + $log.error('Coinbase Get Payment Method: ERROR ' + data.statusText); + return cb(data.data); + }); + }; + + var _post = function(endpoint, token, data) { + return { + method: 'POST', + url: credentials.API + '/v2' + endpoint, + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer ' + token + }, + data: data + }; + }; + + root.sellRequest = function(token, accountId, data, cb) { + var data = { + amount: data.amount, + currency: data.currency, + payment_method: data.payment_method || null, + commit: data.commit || false + }; + $http(_post('/accounts/' + accountId + '/sells', token, data)).then(function(data) { + $log.info('Coinbase Sell Request: SUCCESS'); + return cb(null, data.data); + }, function(data) { + $log.error('Coinbase Sell Request: ERROR ' + data.statusText); + return cb(data.data); + }); + }; + + root.sellCommit = function(token, accountId, sellId, cb) { + $http(_post('/accounts/' + accountId + '/sells/' + sellId + '/commit', token)).then(function(data) { + $log.info('Coinbase Sell Commit: SUCCESS'); + return cb(null, data.data); + }, function(data) { + $log.error('Coinbase Sell Commit: ERROR ' + data.statusText); + return cb(data.data); + }); + }; + + root.buyRequest = function(token, accountId, data, cb) { + var data = { + amount: data.amount, + currency: data.currency, + payment_method: data.payment_method || null, + commit: false + }; + $http(_post('/accounts/' + accountId + '/buys', token, data)).then(function(data) { + $log.info('Coinbase Buy Request: SUCCESS'); + return cb(null, data.data); + }, function(data) { + $log.error('Coinbase Buy Request: ERROR ' + data.statusText); + return cb(data.data); + }); + }; + + root.buyCommit = function(token, accountId, buyId, cb) { + $http(_post('/accounts/' + accountId + '/buys/' + buyId + '/commit', token)).then(function(data) { + $log.info('Coinbase Buy Commit: SUCCESS'); + return cb(null, data.data); + }, function(data) { + $log.error('Coinbase Buy Commit: ERROR ' + data.statusText); + return cb(data.data); + }); + }; + + root.createAddress = function(token, accountId, data, cb) { + var data = { + name: data.name + }; + $http(_post('/accounts/' + accountId + '/addresses', token, data)).then(function(data) { + $log.info('Coinbase Create Address: SUCCESS'); + return cb(null, data.data); + }, function(data) { + $log.error('Coinbase Create Address: ERROR ' + data.statusText); + return cb(data.data); + }); + }; + + root.sendTo = function(token, accountId, data, cb) { + var data = { + type: 'send', + to: data.to, + amount: data.amount, + currency: data.currency, + description: data.description + }; + $http(_post('/accounts/' + accountId + '/transactions', token, data)).then(function(data) { + $log.info('Coinbase Create Address: SUCCESS'); + return cb(null, data.data); + }, function(data) { + $log.error('Coinbase Create Address: ERROR ' + data.statusText); + return cb(data.data); + }); + }; + + // Pending transactions + + root.savePendingTransaction = function(ctx, opts, cb) { + var network = configService.getSync().coinbase.testnet ? 'testnet' : 'livenet'; + storageService.getCoinbaseTxs(network, function(err, oldTxs) { + if (lodash.isString(oldTxs)) { + oldTxs = JSON.parse(oldTxs); + } + if (lodash.isString(ctx)) { + ctx = JSON.parse(ctx); + } + var tx = oldTxs || {}; + tx[ctx.id] = ctx; + if (opts && (opts.error || opts.status)) { + tx[ctx.id] = lodash.assign(tx[ctx.id], opts); + } + if (opts && opts.remove) { + delete(tx[ctx.id]); + } + tx = JSON.stringify(tx); + + storageService.setCoinbaseTxs(network, tx, function(err) { + return cb(err); + }); + }); + }; + + root.getPendingTransactions = function(cb) { + var network = configService.getSync().coinbase.testnet ? 'testnet' : 'livenet'; + storageService.getCoinbaseTxs(network, function(err, txs) { + return cb(err, JSON.parse(txs)); + }); + }; + + return root; + +}); diff --git a/src/js/services/configService.js b/src/js/services/configService.js index 2af73c109..6e0e67871 100644 --- a/src/js/services/configService.js +++ b/src/js/services/configService.js @@ -38,6 +38,11 @@ angular.module('copayApp.services').factory('configService', function(storageSer testnet: false }, + coinbase: { + enabled: true, + testnet: false + }, + rates: { url: 'https://insight.bitpay.com:443/api/rates', }, @@ -93,6 +98,9 @@ angular.module('copayApp.services').factory('configService', function(storageSer if (!configCache.glidera) { configCache.glidera = defaultConfig.glidera; } + if (!configCache.coinbase) { + configCache.coinbase = defaultConfig.coinbase; + } if (!configCache.pushNotifications) { configCache.pushNotifications = defaultConfig.pushNotifications; } @@ -105,6 +113,10 @@ angular.module('copayApp.services').factory('configService', function(storageSer // Disabled for testnet configCache.glidera.testnet = false; + // Coinbase + // Disabled for testnet + configCache.coinbase.testnet = false; + $log.debug('Preferences read:', configCache) return cb(err, configCache); }); diff --git a/src/js/services/profileService.js b/src/js/services/profileService.js index b9e61233b..66e1c8ac7 100644 --- a/src/js/services/profileService.js +++ b/src/js/services/profileService.js @@ -664,8 +664,9 @@ angular.module('copayApp.services') } catch (e) {}; }; - root.unlockFC = function(cb) { - var fc = root.focusedClient; + root.unlockFC = function(opts, cb) { + opts = opts || {}; + var fc = opts.selectedClient || root.focusedClient; if (!fc.isPrivKeyEncrypted()) return cb(); diff --git a/src/js/services/storageService.js b/src/js/services/storageService.js index 60f2e2d3a..50a0c0249 100644 --- a/src/js/services/storageService.js +++ b/src/js/services/storageService.js @@ -223,6 +223,26 @@ angular.module('copayApp.services') storage.remove('glideraToken-' + network, cb); }; + root.setCoinbaseRefreshToken = function(network, token, cb) { + storage.set('coinbaseRefreshToken-' + network, token, cb); + }; + + root.getCoinbaseRefreshToken = function(network, cb) { + storage.get('coinbaseRefreshToken-' + network, cb); + }; + + root.setCoinbaseToken = function(network, token, cb) { + storage.set('coinbaseToken-' + network, token, cb); + }; + + root.getCoinbaseToken = function(network, cb) { + storage.get('coinbaseToken-' + network, cb); + }; + + root.removeCoinbaseToken = function(network, cb) { + storage.remove('coinbaseToken-' + network, cb); + }; + root.setAddressbook = function(network, addressbook, cb) { storage.set('addressbook-' + network, addressbook, cb); }; @@ -247,5 +267,17 @@ angular.module('copayApp.services') storage.remove('txsHistory-' + walletId, cb); } + root.setCoinbaseTxs = function(network, ctx, cb) { + storage.set('coinbaseTxs-' + network, ctx, cb); + }; + + root.getCoinbaseTxs = function(network, cb) { + storage.get('coinbaseTxs-' + network, cb); + }; + + root.removeCoinbaseTxs = function(network, cb) { + storage.remove('coinbaseTxs-' + network, cb); + }; + return root; }); diff --git a/src/js/services/txService.js b/src/js/services/txService.js index fe78066d0..96b2c4bd4 100644 --- a/src/js/services/txService.js +++ b/src/js/services/txService.js @@ -4,9 +4,10 @@ angular.module('copayApp.services').factory('txService', function($rootScope, pr var root = {}; var reportSigningStatus = function(opts) { + opts = opts || {}; if (!opts.reporterFn) return; - var fc = profileService.focusedClient; + var fc = opts.selectedClient || profileService.focusedClient; if (fc.isPrivKeyExternal()) { if (fc.getPrivKeyExternalSourceName() == 'ledger') { @@ -58,9 +59,10 @@ angular.module('copayApp.services').factory('txService', function($rootScope, pr } }; - root.checkTouchId = function(cb) { + root.checkTouchId = function(opts, cb) { + opts = opts || {}; var config = configService.getSync(); - var fc = profileService.focusedClient; + var fc = opts.selectedClient || profileService.focusedClient; config.touchIdFor = config.touchIdFor || {}; if (window.touchidAvailable && config.touchIdFor[fc.credentials.walletId]) { requestTouchId(cb); @@ -69,17 +71,18 @@ angular.module('copayApp.services').factory('txService', function($rootScope, pr } }; - root.prepare = function(cb) { - var fc = profileService.focusedClient; + root.prepare = function(opts, cb) { + opts = opts || {}; + var fc = opts.selectedClient || profileService.focusedClient; if (!fc.canSign() && !fc.isPrivKeyExternal()) return cb('Cannot sign'); // should never happen, no need to translate - root.checkTouchId(function(err) { + root.checkTouchId(opts, function(err) { if (err) { return cb(err); }; - profileService.unlockFC(function(err) { + profileService.unlockFC(opts, function(err) { if (err) { return cb(bwsError.msg(err)); }; @@ -90,8 +93,18 @@ angular.module('copayApp.services').factory('txService', function($rootScope, pr }); }; + root.removeTx = function(txp, opts, cb) { + opts = opts || {}; + var fc = opts.selectedClient || profileService.focusedClient; + + fc.removeTxProposal(txp, function(err) { + return cb(err); + }); + }; + root.createTx = function(opts, cb) { - var fc = profileService.focusedClient; + opts = opts || {}; + var fc = opts.selectedClient || profileService.focusedClient; var currentSpendUnconfirmed = configService.getSync().wallet.spendUnconfirmed; var getFee = function(cb) { @@ -114,16 +127,18 @@ angular.module('copayApp.services').factory('txService', function($rootScope, pr }); }; - root.publishTx = function(txp, cb) { - var fc = profileService.focusedClient; + root.publishTx = function(txp, opts, cb) { + opts = opts || {}; + var fc = opts.selectedClient || profileService.focusedClient; fc.publishTxProposal({txp: txp}, function(err, txp) { if (err) return cb(err); else return cb(null, txp); }); }; - var _signWithLedger = function(txp, cb) { - var fc = profileService.focusedClient; + var _signWithLedger = function(txp, opts, cb) { + opts = opts || {}; + var fc = opts.selectedClient || profileService.focusedClient; $log.info('Requesting Ledger Chrome app to sign the transaction'); ledger.signTx(txp, fc.credentials.account, function(result) { @@ -138,8 +153,9 @@ angular.module('copayApp.services').factory('txService', function($rootScope, pr }); }; - var _signWithTrezor = function(txp, cb) { - var fc = profileService.focusedClient; + var _signWithTrezor = function(txp, opts, cb) { + opts = opts || {}; + var fc = opts.selectedClient || profileService.focusedClient; $log.info('Requesting Trezor to sign the transaction'); var xPubKeys = lodash.pluck(fc.credentials.publicKeyRing, 'xPubKey'); @@ -152,15 +168,16 @@ angular.module('copayApp.services').factory('txService', function($rootScope, pr }); }; - root.sign = function(txp, cb) { - var fc = profileService.focusedClient; + root.sign = function(txp, opts, cb) { + opts = opts || {}; + var fc = opts.selectedClient || profileService.focusedClient; if (fc.isPrivKeyExternal()) { switch (fc.getPrivKeyExternalSourceName()) { case 'ledger': - return _signWithLedger(txp, cb); + return _signWithLedger(txp, opts, cb); case 'trezor': - return _signWithTrezor(txp, cb); + return _signWithTrezor(txp, opts, cb); default: var msg = 'Unsupported External Key:' + fc.getPrivKeyExternalSourceName(); $log.error(msg); @@ -175,10 +192,11 @@ angular.module('copayApp.services').factory('txService', function($rootScope, pr }; root.signAndBroadcast = function(txp, opts, cb) { + opts = opts || {}; reportSigningStatus(opts); - var fc = profileService.focusedClient; - root.sign(txp, function(err, txp) { + var fc = opts.selectedClient || profileService.focusedClient; + root.sign(txp, opts, function(err, txp) { if (err) { stopReport(opts); return cb(bwsError.msg(err), gettextCatalog.getString('Could not accept payment')); @@ -208,7 +226,8 @@ angular.module('copayApp.services').factory('txService', function($rootScope, pr }; root.prepareAndSignAndBroadcast = function(txp, opts, cb) { - root.prepare(function(err) { + opts = opts || {}; + root.prepare(opts, function(err) { if (err) { stopReport(opts); return cb(err); diff --git a/util/coinbase.js b/util/coinbase.js new file mode 100755 index 000000000..d0bd4006d --- /dev/null +++ b/util/coinbase.js @@ -0,0 +1,20 @@ +#!/usr/bin/env node + +'use strict'; + +var fs = require('fs'); +var file; + +try { + file = fs.readFileSync('./coinbase.json', 'utf8'); +} catch(err) { + return; +} + +var json = JSON.parse(file); +console.log('Coinbase Client ID: ' + json.client_id); + +var content = 'window.coinbase_client_id="' + json.client_id + '";'; +content = content + '\nwindow.coinbase_client_secret="' + json.client_secret + '";'; +fs.writeFileSync("./src/js/coinbase.js", content); +